Skip to content

ADR-0009 · Business-vertical archetype platform (metadata-driven verticals)

Status: 🟢 Partially implemented — the taxonomy + archetype + capability layer is BUILT (2026-08-01, see the Implementation addendum at the bottom); Storage-C's item/transactional spine is not · direction endorsed by product 2026-07-20 · Depends on: ADR-0001, ADR-0007 · Drives:ADR-0003, ADR-0004

Read the addendum before designing against this document

Two things in the body below are now superseded by what was actually built, and one is withdrawn:

  • the implied one-capability-set-per-archetype model (activates) — wrong, capabilities are composable;
  • open question 4 (BioLink as a 6th archetype) — moot, BioLink was retired by QRS-172;
  • "Membership / Subscription-to-merchant" as a candidate archetype — it is a capability, not an archetype.

The one-line thesis

A business vertical is configuration, not code. Onboarding a new category (real estate → interior designer → travel agent) must be data entry — an archetype selection, a field schema, a card template, a feature set, an entitlement default set, and a compliance profile — never a new table, migration, or code path. This ADR defines the abstraction that makes that true.

Context

The R1 domain-selection debate (2026-07-20) converged on a principle bigger than the domain list itself: QRSETU should scale to arbitrary business verticals without per-vertical engineering. The six confirmed R1 domains (Salon, Real Estate, Carpenter, Purohit, Loan Agent, Car Dealer/Salesperson) plus the near-term shortlist (Interior Designer, Photographer, Event Planner, Fitness, Tutor, CA, Doctor/Clinic, Boutique, Home Chef, Travel Agent) do not each deserve bespoke data models, screens, or migrations. They collapse onto a small set of archetypes — reusable data-model + capability + entitlement shapes — and differ only by configuration.

The schema already leans this way, which is why this is a formalization, not a rewrite:

  • profiles carries business_domain_id, template_id, and a general-purpose attributes jsonb — the bones of a config-driven, schema-flexible business record already exist.
  • business_domains (vertical registry), domain_features (feature × domain junction), and platform_features (canonical feature registry, feature_code) exist — the vertical + capability metadata layer is partly built.
  • The current useMenuManagerEligibility hardcodes business_domain_id === 1 — the anti-pattern this ADR exists to kill: a vertical encoded in code instead of data.

What the domain debate taught us (design inputs, now decisions)

  • Archetype, not domain, is the unit of engineering. Adding Car Dealer after Real Estate costs ~nothing — same inventory archetype. Adding a genuinely new archetype (e-commerce cart) is the real work. So R1 cost is driven by archetype count (~5), not domain count.
  • Compliance varies per vertical and must be first-class config. Loan Agent → private/invite + KYC + verified badge before public marketing. CA / Doctor / Lawyer → advertising-restricted by their professional bodies (ICAI / NMC / BCI), so their surfaces must be informational/discovery-oriented, not self-promotional, and (for doctors) store no patient records / prescriptions / sensitive medical data in current scope. A vertical therefore carries a compliance profile, not just features.
  • Some verticals are distribution channels, not just users (a CA has 100+ SMB clients; an event planner coordinates 15–20 vendors). The model should not preclude a business referring/onboarding other businesses later (ties ADR-0001 organizations and ADR-0005).

Decision drivers

  • Zero-code vertical onboarding — the explicit product goal: a new category is command-center data entry.
  • Don't fork the data model per vertical — 20 verticals must not mean 20 entity tables or a migration per launch.
  • Reuse the transactional spine — an appointment is an appointment whether salon or clinic; an order is an order whether boutique or home-chef. Cross-archetype transactional tables, not per-domain ones.
  • One source of truth per concern — a vertical's field schema should drive validation and form rendering and card rendering, not be re-declared three times.
  • Compose with the decided ADRs — entitlements (ADR-0007) key their limits off the archetype's primary entity; templates (ADR-0003) are archetype-aware; ads (ADR-0004) respect the compliance profile; RBAC (ADR-0006) still gates by capability.
  • TypeScript-first, portable core — archetype definitions, field schemas (Zod), and resolvers live in the shared platform-agnostic core so web and the future Expo app consume one definition.

Two orthogonal axes (the core insight)

QRSETU screens have historically conflated what a business is with how it's shown. Separate them:

AxisQuestionValues (R1)
Archetype (data model)What does the business manage?Inventory · Booking · Lead/Portfolio · Catalog/Informational · E-commerce
Surface (presentation)How is it shown to end-customers?Service Card · Mini-website (Studio) · BioLink · QR

A vertical picks one archetype and is presentable on any surface. Salon = Booking archetype shown as a Service Card + QR; a boutique = E-commerce archetype shown as a Mini-website + BioLink. This orthogonality is why the three product pillars (Interaction / Presence / Aggregation) are surfaces, not verticals — any archetype flows through any of them.

The archetype catalog

R1 archetypes (the ~5 that gate engineering cost)

ArchetypePrimary entityEntitlement limits key offR1 domainsNear-term domains
Inventory / Listinglisting (rich, browsable, per-item lead)# listings, photos/listingReal Estate, Car DealerRentals, Property mgmt
Booking / Appointmentservice + appointment# services, staff, appts/mo, gallerySalonFitness/Yoga, Doctor/Clinic, Consultant
Lead / Portfolioportfolio_item + leadportfolio photos, service-areas, leads/moCarpenter, Loan AgentInterior Designer, Photographer, Insurance
Catalog / Informationalservice/offering (info-first, low quota)# offerings; compliance-gatedPurohitCA, Tutor, Lawyer
E-commerce / Cartproduct + order# products, orders/mo, paymentBoutique, Home Chef/Baker, Retail

Candidate archetypes (post-R1, named so the model leaves room)

  • Link-aggregation / Creator — the existing BioLink pillar as an archetype (links, socials, embeds). Likely is a surface + a thin archetype; reconcile with the pillar rather than reinvent.
  • Membership / Subscription-to-merchant — the merchant sells their own recurring plans (gym memberships, tiffin subscriptions). Distinct because it makes the merchant a sub-merchant-of-record — real Razorpay-routing/marketplace complexity. Defer, but don't design it out.
  • Events / Ticketing — RSVP/seats/tickets. Initially a variant of E-commerce (ticket = product) or Booking (seat = slot); promote to its own archetype only if demand is real.
  • Donation / Collection — NGO/temple/dakshina; a "collect payment, no product" primitive that Purohit and others can switch on.

Digital Menu (hospitality) is a specialization of Catalog/E-commerce, not a new archetype — see Consequences.

Options considered — how a vertical's data is stored

The crux. A vertical's descriptive data (a property's bedrooms, a service's duration, a product's SKU) varies wildly; its transactional data (an appointment, a lead, an order) is remarkably uniform across verticals.

Storage-A — One entity table per vertical (properties, salon_services, …)

  • For: clean relational integrity per vertical; indexable typed columns.
  • Against: every new vertical is a migration + new code path — the exact thing we're eliminating. 20 verticals = 20 tables + 20 sets of CRUD. Rejected — fails the zero-code goal outright.

Storage-B — Pure EAV (entity-attribute-value) / one giant attributes blob for everything

  • For: infinitely flexible; zero schema per vertical.
  • Against: no integrity, no typed indexes, unqueryable at scale, and transactional data (orders, payments) has no business being schemaless. Rejected for transactional data; too loose even for descriptive data alone.

Three layers:

  1. A common item spine — business_items. One polymorphic table for the descriptive primary entity of the Inventory/Catalog/Lead-Portfolio/E-commerce archetypes and the "service" of Booking. Typed common columns that nearly every vertical needs — id, business_id, domain_id, archetype, item_type, title, subtitle, description, price_amount, price_currency, cover_media, status, is_published, position, created_at/updated_atplus attributes jsonb for the vertical-specific long tail (bedrooms, fuel_type, duration_min, SKU, muhurat_types). Common fields are columns (indexed, queryable); the tail is validated jsonb.
  2. A per-domain field schema (schema-as-data). Each business_domain carries a versioned JSON-Schema/Zod definition of its attributes shape. This single definition drives validation (EF + DB), form rendering (onboarding + item editor), and card rendering (which attributes show, how) — one source of truth, stored as data so the command center edits it. New vertical = new schema row, no migration.
  3. Shared transactional tables, activated by archetype (never per-domain). leads/inquiries (Inventory, Lead-Portfolio, Catalog), appointments (Booking), orders + order_items (E-commerce), item_media (all). These are fully normalized and domain-agnostic — an appointment is an appointment. A new vertical reuses these via its archetype; only a brand-new archetype (rare) adds a transactional table.
  • For: new vertical = config (archetype + schema + template + features + entitlements + compliance) with no migration; common fields stay typed/indexed/queryable; transactional data keeps full integrity; matches what profiles.attributes jsonb already hints at; the field schema is reused three ways instead of re-declared.
  • Against: jsonb attributes need disciplined app-level validation (mitigated: the schema is the validator); querying deep into attributes needs GIN indexes / generated columns for hot filters (acceptable, well-trodden Postgres); requires a schema-versioning discipline (mitigated by expand-contract on the schema doc).

Options considered — how a vertical is defined

Code-defined verticals (a TS registry compiled in) vs data-defined verticals (config rows in business_domains + schema + entitlement matrix). Data-defined wins — it's the entire point; a code registry would put every launch back behind a deploy. (A thin code layer still maps archetype → transactional behavior; that's per-archetype, not per-vertical, so it's ~5 implementations, not N.)

Recommendation

Adopt the two-axis, archetype-driven, metadata-defined model with Storage-C:

  1. vertical_archetype (≈5 rows, code-backed behavior): declares the primary entity, which shared transactional tables it activates, and its default entitlement-limit keys.
  2. business_domains (the vertical registry, all data): each row = { archetype, field_schema (versioned), default_card_template, feature_set, entitlement_defaults, compliance_profile, onboarding_steps, status(public|private|invite) }. This row is the whole vertical. Adding one is data entry.
  3. business_items + item_media — the common descriptive spine (common columns + validated attributes).
  4. Shared transactional tablesleads, appointments, orders/order_items — archetype-activated, domain-agnostic.
  5. Compliance profile (first-class on the domain): { ad_restricted, marketing_restricted, kyc_required, verified_badge_required, visibility(public|private|invite), stores_sensitive_data }. Gates copy tone (informational vs promotional for CA/Doctor), ad injection (ADR-0004 must honor ad_restricted), and launch mode (Loan Agent = invite + kyc_required day one).
  6. Entitlements (ADR-0007) key their limits off the archetype's primary entitylistings, appointments_per_month, products, portfolio_photos are feature_codes whose per-(domain,tier) limits live in the matrix. This ADR supplies what entities exist per archetype; ADR-0007 supplies how many each tier gets.
  7. Templates (ADR-0003) declare archetype compatibility and render from business_items + the field schema. A vertical's default_card_template is config.
  8. One shared-core layer: archetype definitions + Zod field schemas + an useVertical()/resolver so the item editor, onboarding, and card renderer are all schema-driven. TypeScript, no exceptions (see Consequences).

Onboarding a new vertical end-to-end becomes: pick archetype → author field schema → pick/assign card template → select feature set + entitlement defaults → set compliance profile → publish. No migration, no deploy.

Consequences

  • Digital Menu is deferred out of R1 and re-homed onto this model. It is not a special feature — it's the Catalog/E-commerce archetype (menu = product catalog; KOT/orders = the order table; cash-calculator = a tool). Since it's not in production for any customer, its schema can be evolved freely to become the reference instance of the E-commerce archetype in R2, with a dedicated migration/integration plan. This both validates the archetype model on real, mature feature code and removes the "menu is bespoke" debt. Tracked as an R2 workstream.
  • useMenuManagerEligibility's business_domain_id === 1 gate dies — replaced by archetype + entitlement resolution. (Also an ADR-0007 item.)
  • New transactional tables (business_items, item_media, leads, appointments, orders, order_items) ship as expand-contract migrations with RLS on every table, promoted Dev→Prod per the runbook. This is the one real schema build — after it, verticals are data.
  • The admin command center (ADR-0007) grows a "Verticals" section — CRUD over business_domains: archetype, field-schema editor, template assignment, feature set, entitlement defaults, compliance profile. This is where a non-engineer launches a vertical.
  • Compliance is enforced, not documented: ad_restricted domains suppress ad slots regardless of ADR-0004 config; marketing_restricted domains get informational card copy (protects CA/Doctor under ICAI/NMC); invite+kyc_required gates Loan Agent's public exposure while your design-partner user runs live. Confirm exact ICAI/NMC/BCI rules per body before shipping copy for those verticals.
  • TypeScript strict is reaffirmed as non-negotiable for all new code, and a JS→TS migration roadmap is adopted for the remaining core modules (Digital Menu migrates under its own R2 plan; the rest progressively). Archetype definitions, field schemas, and resolvers are TS in the shared core from day one. Tracked.
  • Ads decision (product, 2026-07-20): third-party ad/promotional placements on cards/dashboards are acceptable where compliant — so ad-eligibility becomes a per-domain compliance flag (ad_restricted) feeding ADR-0004, not a global on/off.
  • Query performance: hot attributes filters (e.g. real-estate "3 BHK under ₹1cr") need GIN indexes or generated columns; budget for it in the Inventory archetype specifically.

Open questions for product / engineering

  1. Resolved (2026-07-20): all 5 archetypes ship in R1, including E-commerce (product overrode the R2 recommendation). The E-commerce archetype (cart/orders/payment/inventory-decrement) is built in R1 even though its first domains — Boutique, Home Chef, Retail — are on the near-term list rather than the 6 confirmed R1 domains; Digital Menu's R2 migration then rides on the same archetype. This makes R1 heavier (the costliest archetype is in scope) — sequenced against the mobile-first frontend work (see the frontend-platform ADR).
  2. Resolved (2026-07-20): engineering-seeded JSON field schemas for R1; a visual schema-builder in the command center is a post-R1 fast-follow.
  3. business_items vs archetype-named views: one physical table with archetype/item_type discriminators (recommended) vs updatable views per archetype for ergonomics? (Views are a nicety; decide at build.)
  4. BioLink reconciliation: is Link-aggregation a 6th archetype or purely a surface over a thin profile? Resolve with the Aggregation-pillar owner.
  5. Membership/subscription-to-merchant: confirm it's out of R1/R2 scope (it changes Razorpay integration shape — sub-merchant/marketplace, ADR-0002).
  • ADR-0001 — identity/workspace the business record hangs off; connector verticals touch the future Organization layer.
  • ADR-0007 — entitlement limits key off this ADR's primary entities; the command center hosts both verticals and the entitlement matrix.
  • ADR-0003 — templates become archetype-aware and render from business_items.
  • ADR-0004 — ad injection honors the per-domain ad_restricted compliance flag.
  • ADR-0006 — capability gate composes with entitlement + applicability.
  • Tracker: business_domain_id===1 hardcoded gate; Digital Menu R1 exclusion + R2 archetype migration; JS→TS roadmap; archetype-platform schema build (documentation/portal/dev-tracker/tracker.md).

Implementation addendum (2026-08-01)

What was actually built, what it corrected in the body above, and what is deliberately still absent. Written at implementation time rather than left to be re-derived — this ADR sat endorsed-but-unbuilt for six weeks and the schema drifted the other way in the meantime.

What triggered it

The mobile app hardcoded 12 occupation-level verticals (constants/domains.ts, ids 1-12). business_domains on Prod held 10 sector-level rows, also ids 1-10. Same id space, different taxonomies, and a real fk_profiles_business_domain between them. Consequence, had onboarding ever written successfully: 10 of 12 industry choices would have recorded the wrong industry (id 2 = "Salon / Spa" in the app, "Retail & Shopping" in the database) and ids 11-12 would have failed the FK outright. Caught before any of it reached real data — the new app had never successfully written the column.

What exists now

ObjectMigrationNotes
vertical_archetypes20260801085211The 5 R1 archetypes, with primary_entity.
business_domains.archetype_id + status20260801085211status is the ADR's public/private/invite; it replaced a boolean that could not express invite-only.
12 verticals (ids 11-22)20260801084314 + ...5211Ids pinned explicitly, never nextval — see below.
10 legacy sectors retired20260801085211is_active=false, kept (not dropped) because domain_features still FKs them.
capabilities · archetype_default_capabilities · profile_capabilities20260801091104Composable capability model — see below.
get_business_domains() · get_my_capabilities()...5211 / ...1104SECURITY DEFINER, projection-only, least-privilege grants per ADR-0014.

Ids are pinned, and that is load-bearing. profiles.business_domain_id is persisted and promoted across environments. If Dev and Prod each seeded from a sequence, the same integer would mean different things per environment — the same class of bug this work exists to fix, one level up.

Sector rows are retired, not deleted, and carry no parent_id. An earlier revision added parent_id as a sector rollup. That was wrong and was dropped within the same migration series: sector is a human-browse taxonomy, whereas this ADR's rollup is vertical to archetype, a behavioural contract. The two group differently and the difference matters — Electrician and Purohit share a sector but have different archetypes (Lead/Portfolio vs Catalog/Informational).

Correction: capabilities are composable, not per-archetype

The body of this ADR implies each archetype activates a fixed set of transactional tables, and the first implementation encoded that as vertical_archetypes.activates. Tested against real Indian SMB workflows it is wrong, and always in the same direction — too restrictive:

Real workflowNeedsArchetype it belongs to
Electrician / plumber annual maintenance contractorders + subscriptionsLead/Portfolio
Photographer's fixed-price wedding packageordersLead/Portfolio
Gym or yoga membershipsubscriptionsBooking
Purohit puja package · tutor course feeorders (+ subscriptions)Catalog/Informational
Real-estate token / booking amountordersInventory
Salon prepaid packages + retail shelfordersBooking
Site visits, test drives, consultations, table bookings, delivery slotsappointmentsall five

Every capability applies to every archetype. A per-archetype capability list is therefore not a meaningful discriminator, and freezing one as a constraint reintroduces exactly the rigidity this ADR exists to remove: "electricians can now sell maintenance plans" would have required an archetype change instead of a toggle.

What genuinely distinguishes an archetype is only (1) its primary_entity and (2) sensible defaults, so a salon is not asked to configure a cart on day one. activates is dropped; archetype_default_capabilities holds defaults, explicitly not limits, with every archetype x capability pair stated explicitly so an "off" is always a recorded decision rather than an oversight.

ArchetypeOn at signupAvailable, off by default
Inventory / Listingcatalog, media, leads, appointmentsorders, subscriptions
Booking / Appointmentcatalog, media, leads, appointments, orderssubscriptions
Lead / Portfoliocatalog, media, leads, appointmentsorders, subscriptions
Catalog / Informationalcatalog, media, leads, appointmentsorders, subscriptions
E-commerce / Cartcatalog, media, leads, ordersappointments, subscriptions

subscriptions is off everywhere, and for a payments reason rather than a business-type one: merchant-collected recurring billing needs Razorpay sub-merchant routing, which ADR-0002 has not built. Gyms, tiffin services and AMC providers all genuinely need it; it flips on when that lands, with no schema change.

Capability is not entitlement (the line that keeps this from colliding with ADR-0007)

  • Capability (here, structural): does this workflow apply to this business at all — is there an Appointments section in this merchant's app? Resolution is per-merchant override, then archetype default, then false — expressed once in get_my_capabilities() so the client cannot drift from it.
  • Entitlement (ADR-0007, commercial): is it unlocked at this tier, and how many?

A free-tier cafe has orders and is capped at N per month. Neither answers the other's question, so neither replaces the other. domain_features was deliberately left untouched for this reason: ADR-0007's revised decision is a normalized (domain x tier x feature) matrix, and an archetype_features table would have contradicted it.

Deliberately NOT built

  • business_items / item_media / leads / appointments / orders / order_items — Storage-C's spine. Nothing writes them yet, and designing the cart before a feature needs it is the speculative abstraction CLAUDE.md forbids. They ship with the first feature that consumes them, as this ADR already schedules.
  • field_schema (schema-as-data) and compliance_profile — both first-class in the body above, both with no consumer today. Add them with the item editor and the ad-injection work respectively.
  • The command center's Verticals section — belongs to the ADR-0007 workstream.

Corrections to this document's own open questions

  • #4 (BioLink as a 6th archetype) is MOOT. QRS-172 retired BioLink in favour of the Setu Card (2026-07-23). No reconciliation is needed; the Aggregation pillar is a surface, as the two-axis model already says.
  • "Membership / Subscription-to-merchant" is not a candidate archetype. It is a capability cutting across all five — a gym is Booking-with-subscriptions, a tiffin service is E-commerce-with-subscriptions. The real concern named there (sub-merchant Razorpay routing) is a payments problem, and remains valid as one.
  • #1 and #2 are already resolved in the body and simply were not re-read; noted so the next reader does not re-litigate them.

Consumers wired to this

packages/data/src/domains/ (DomainsService, real + stub, ids mirroring the migration), the app's useBusinessDomains() hook, and IndustryStep / Profile > Business. apps/mobile/.../constants/domains.ts is deleted. Contract tests pinning the real ids live in apps/mobile/src/tiers/user/features/onboarding/__tests__/domains-contracts.test.ts — they exist because the original defect was two sources of truth agreeing with each other and disagreeing with the database.