Appearance
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:
profilescarriesbusiness_domain_id,template_id, and a general-purposeattributes jsonb— the bones of a config-driven, schema-flexible business record already exist.business_domains(vertical registry),domain_features(feature × domain junction), andplatform_features(canonical feature registry,feature_code) exist — the vertical + capability metadata layer is partly built.- The current
useMenuManagerEligibilityhardcodesbusiness_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:
| Axis | Question | Values (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)
| Archetype | Primary entity | Entitlement limits key off | R1 domains | Near-term domains |
|---|---|---|---|---|
| Inventory / Listing | listing (rich, browsable, per-item lead) | # listings, photos/listing | Real Estate, Car Dealer | Rentals, Property mgmt |
| Booking / Appointment | service + appointment | # services, staff, appts/mo, gallery | Salon | Fitness/Yoga, Doctor/Clinic, Consultant |
| Lead / Portfolio | portfolio_item + lead | portfolio photos, service-areas, leads/mo | Carpenter, Loan Agent | Interior Designer, Photographer, Insurance |
| Catalog / Informational | service/offering (info-first, low quota) | # offerings; compliance-gated | Purohit | CA, Tutor, Lawyer |
| E-commerce / Cart | product + order | # products, orders/mo, payment | — | Boutique, 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.
Storage-C — Hybrid: common-core columns + validated jsonb attributes + shared transactional tables · [recommended]
Three layers:
- 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_at— plusattributes jsonbfor the vertical-specific long tail (bedrooms, fuel_type, duration_min, SKU, muhurat_types). Common fields are columns (indexed, queryable); the tail is validated jsonb. - A per-domain field schema (schema-as-data). Each
business_domaincarries a versioned JSON-Schema/Zod definition of itsattributesshape. 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. - 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 jsonbalready 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
attributesneeds 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:
vertical_archetype(≈5 rows, code-backed behavior): declares the primary entity, which shared transactional tables it activates, and its default entitlement-limit keys.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.business_items+item_media— the common descriptive spine (common columns + validatedattributes).- Shared transactional tables —
leads,appointments,orders/order_items— archetype-activated, domain-agnostic. - 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 honorad_restricted), and launch mode (Loan Agent =invite+kyc_requiredday one). - Entitlements (ADR-0007) key their limits off the archetype's primary entity —
listings,appointments_per_month,products,portfolio_photosarefeature_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. - Templates (ADR-0003) declare archetype compatibility and render from
business_items+ the field schema. A vertical'sdefault_card_templateis config. - 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'sbusiness_domain_id === 1gate 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_restricteddomains suppress ad slots regardless of ADR-0004 config;marketing_restricteddomains get informational card copy (protects CA/Doctor under ICAI/NMC);invite+kyc_requiredgates 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
attributesfilters (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
- ✅ 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).
- ✅ 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.
business_itemsvs archetype-named views: one physical table witharchetype/item_typediscriminators (recommended) vs updatable views per archetype for ergonomics? (Views are a nicety; decide at build.)- BioLink reconciliation: is Link-aggregation a 6th archetype or purely a surface over a thin profile? Resolve with the Aggregation-pillar owner.
- Membership/subscription-to-merchant: confirm it's out of R1/R2 scope (it changes Razorpay integration shape — sub-merchant/marketplace, ADR-0002).
Related
- 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_restrictedcompliance flag. - ADR-0006 — capability gate composes with entitlement + applicability.
- Tracker:
business_domain_id===1hardcoded 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
| Object | Migration | Notes |
|---|---|---|
vertical_archetypes | 20260801085211 | The 5 R1 archetypes, with primary_entity. |
business_domains.archetype_id + status | 20260801085211 | status is the ADR's public/private/invite; it replaced a boolean that could not express invite-only. |
| 12 verticals (ids 11-22) | 20260801084314 + ...5211 | Ids pinned explicitly, never nextval — see below. |
| 10 legacy sectors retired | 20260801085211 | is_active=false, kept (not dropped) because domain_features still FKs them. |
capabilities · archetype_default_capabilities · profile_capabilities | 20260801091104 | Composable capability model — see below. |
get_business_domains() · get_my_capabilities() | ...5211 / ...1104 | SECURITY 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 workflow | Needs | Archetype it belongs to |
|---|---|---|
| Electrician / plumber annual maintenance contract | orders + subscriptions | Lead/Portfolio |
| Photographer's fixed-price wedding package | orders | Lead/Portfolio |
| Gym or yoga membership | subscriptions | Booking |
| Purohit puja package · tutor course fee | orders (+ subscriptions) | Catalog/Informational |
| Real-estate token / booking amount | orders | Inventory |
| Salon prepaid packages + retail shelf | orders | Booking |
| Site visits, test drives, consultations, table bookings, delivery slots | appointments | all 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.
| Archetype | On at signup | Available, off by default |
|---|---|---|
| Inventory / Listing | catalog, media, leads, appointments | orders, subscriptions |
| Booking / Appointment | catalog, media, leads, appointments, orders | subscriptions |
| Lead / Portfolio | catalog, media, leads, appointments | orders, subscriptions |
| Catalog / Informational | catalog, media, leads, appointments | orders, subscriptions |
| E-commerce / Cart | catalog, media, leads, orders | appointments, 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) andcompliance_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.