Appearance
ADR-0001 · Tenancy & identity model
Status: 🟢 Confirmed (2026-08-04) — Option B's shape decided; not yet built, verified against Dev · Depends on: — · Drives: ADR-0002
Supersession / amendment notice
SUPERSEDED IN PART by ADR-0020 (2026-08-07). This ADR models an organization as a billing wrapper over independent profiles — a shape forced by profiles conflating person, business, card, preferences and billing in one row. ADR-0020 D1/D4 replace it: the principal is a user, the workspace is the business tenant, a solo merchant is a one-member workspace, and an individual consumer is a principal with zero memberships. Read this ADR for the tenancy requirements it captures, not for its table shape.
Context
The real backend has no tenant concept above a single user. profiles is the one and only identity hub (see Database & RLS) — one row per auth user, and every domain table across all three pillars hangs directly off profiles.id/user_id. There is no accounts, workspaces, organizations, or members table anywhere in the 58-table baseline schema.
The Claude Designs prototype has already designed past this. ui_kits/merchant-console-v2/CONTRACTS.md §2, explicitly labeled "Track B of the approved plan", specifies:
Account → Workspace(tenant) → Member(role)— one account may own or belong to many workspaces (a personal card plus a shop; a teacher's card plus a school club), every domain entity carries a workspaceId, and the workspace switcher changes the active tenant. This isn't scope creep invented mid-prototype — it's a contract the approved plan called for building toward. But it does not exist in the real schema, and several already-reviewed screens assume it does:
- Subscriptions' Custom Deals tab has org/seat pricing UI with no seat/department/multi-location data model behind it (tracker: "No Organization-above-Workspace entity").
- AdManager and the merchant console (mobile vs desktop) both assume a workspace switcher exists; the desktop console derives nav from
manifest.computeNav(workspace), mobile hardcodes a single workspace. - The dual-audience decision (locked 2026-07-14) requires individuals to use generic features with no business profile at all — so whatever model is chosen must not force a workspace/org onto every user.
Decision drivers
- Don't break individuals (biolink/qr-generator/business-card/reminders users with no business) — a workspace concept must stay optional, not mandatory.
- Franchise/multi-location/organization billing (Subscriptions' Custom Deals) needs a real entity above a single profile to attach seat counts and consolidated invoicing to.
- RLS currently isolates by
profile_id/user_id. Multi-member workspaces (staff logging into a shared business account) need isolation by workspace membership instead — a genuinely large RLS rewrite once it happens, not a small one. - Mobile north-star (Expo, shared backend) means whatever model is picked has to work for a native client too, not just the web console.
- Migration blast radius: retrofitting
workspace_idonto most of the 58-table schema is the single largest migration this program would undertake so far.
Options considered
A — Build the full Account → Workspace → Member model now
Add accounts, workspaces, workspace_members tables; every domain table gains a workspace_id (nullable → backfilled from the owning profile → eventually the isolation key); RLS rewritten to check workspace membership instead of auth.uid() = user_id.
-For: matches CONTRACTS.md exactly; unlocks staff/multi-role access and org-level billing in one pass.
- Against: by far the largest migration in this program's history — touches nearly every RLS policy in the schema — undertaken before a single paying customer has asked for multi-seat access.
B — Minimal Organization wrapper, billing-only, no multi-member RBAC yet
Add one lightweight organizations table; profiles.organization_id (nullable). Used only to attach seat count and consolidated/franchise billing to a group of profiles. profiles stays the operative identity for everything else — RLS, ownership, and every existing table are untouched.
- For: small, additive, expand-contract-friendly migration (one table, one nullable FK); unblocks Subscriptions' org/seat pricing immediately; zero impact on individuals (
organization_idstays null for them); doesn't touch a single existing RLS policy. - Against: doesn't give the console's workspace-switcher/staff-role UI a real backend — those screens stay presentational-only until a later phase.
C — Defer entirely
Keep profiles flat. Treat the prototype's org/seat and workspace-switcher UI as prototype-only scope, not committed to being built, until a real franchise customer signs up.
- For: zero migration cost today.
- Against: contradicts
CONTRACTS.md, which was already ratified as an approved-plan track — this would be reopening a decision rather than making a new one, and lets prototype/backend divergence keep growing as more admin screens get designed against a tenant model that isn't there.
Recommendation
B now, A staged behind real demand. Ship the lightweight organizations wrapper: it unblocks real subscription/billing data (ADR-0002 depends on it), costs one small additive migration, and — critically — never forces a workspace/organization concept onto an individual user, which the locked dual-audience decision requires. Explicitly do not build the full Workspace/Member/staff-role RBAC layer, or rewrite RLS around it, until multi-seat demand is real. Keep CONTRACTS.md's fuller model as the named target architecture for that future phase — this ADR doesn't reject it, it sequences it behind evidence instead of behind the prototype's UI.
Consequences
- ADR-0002 (subscription/billing) depends on the
organizationstable existing. - The console's workspace-switcher and staff-role UI (desktop
ui_kits/merchant-console-v2, AdManager's targeting-by-workspace assumptions) should be explicitly labeled "designed ahead of backend, Phase 2" rather than implied as already-backed. - No existing RLS policy, table, or
[ENFORCED]data-access rule changes as a result of adopting B.
Decision (2026-07-19) — workspace identity = business name, fallback member name
Product confirmed the identity contract for the eventual organizations/workspace layer (and for how the flat profiles row is labelled today): a workspace is identified by its business name, falling back to the individual/member name for individual accounts. Mapped to the real schema (verified against the baseline profiles table, 20260710134136_…:1328):
- Business/brand name →
profiles.brand_name(text). This is the workspace label for a business account. - Individual/member name →
profiles.full_name(text). This is the fallback label when there is no business (the dual-audience "individual" case), and it is also the account owner's name for a business. - There is no
business_nameordisplay_namecolumn — do not invent one;brand_nameis the business name. (A third genericprofiles.name varchar(255)exists but is the public-page entity name, not surfaced in Profile/Settings — leave it out of the identity label.) - When the
organizationswrapper (option B) is built,organizations.nameseeds frombrand_name; an individual never gets anorganizationsrow, so their identity resolves tofull_name— this is exactly what keeps a workspace/org concept from being forced onto individuals.
This also means the current onboarding flow (which collects both full_name and brand_name from every user with no business-vs-individual fork) diverges from the locked dual-audience decision — an individual should not be required to supply a brand_name. That reconciliation is specced in the Profile/Settings/Onboarding brief and logged in the tracker.
Open question for product
What actually triggers building the full Workspace/Member model — a specific paying customer requesting multi-seat/staff login, or a fixed roadmap date regardless of demand? This ADR assumes the former; confirm before treating Phase 2 as scheduled. (The D3 naming decision above settles how the layer identifies a workspace; it does not by itself schedule the build.)
Confirmation (2026-08-04) — Option B's shape, and answering the open question above
Product confirmed, during the payment-architecture review, which of two genuinely different models "seat-based licensing" and "team/organization workspaces" mean for QRSetu's near-term roadmap (car dealerships buying seats for reps, MLM uplines buying seats for downline distributors — both already identified as real, near-term customer segments):
Confirmed target: Option B's org, as a BILLING relationship over independent profiles — not the full Account→Workspace→Member model. Each seat is its own profiles row with its own login, own Setu Card, own business identity. An organizations row attaches a billing relationship to N such profiles — who pays, and (for ownership_model: 'org_owned') whether the org can revoke a seat. There is no shared login and no staff-role RBAC inside one business account in this model — that remains the deferred Workspace/Member layer this ADR already named, and it answers the open question directly: the trigger is a specific paying customer requesting multi-seat staff login into one shared business, not a roadmap date, and no such customer has been identified yet. The dealership/MLM segments that prompted this confirmation both want Model B (independent, billed-for profiles), not shared staff login.
Verified, not assumed, before writing this section: none of it is built yet. Checked directly against Dev on 2026-08-04 — no organizations table, no profiles.organization_id column, no billing_accounts table, no get_my_entitlements RPC. The "six decisions" sketched in an earlier planning pass were recorded as intent and never executed. This section replaces that intent with a dated, confirmed decision plus an honest build-status, so the next reader does not have to re-derive either.
What needs zero change for this model, verified by tracing the mechanics rather than asserting it:item_payments (QRS-312) and payment_kill_switch (QRS-315) — the Razorpay Route payment layer built 2026-08-03 — require no rework. A dealership rep's payment split still settles to that rep's own linked Route account regardless of who pays the rep's QRSetu subscription; item_payments.vendor_profile_id already points at profiles.id, and an org's billing relationship to that same profile is a layer on top of it, not a change to it. This is the concrete answer to "will today's payment schema need rework" — no, by construction, because Flow A (vendor→QRSetu billing) and Flow B (customer→vendor payment) never shared a table to begin with.
What ships additively, with the first feature that needs it (not now — no consumer exists today, and building it without one is the speculative abstraction CLAUDE.md forbids, the same discipline ADR-0009 already applied to business_items/appointments):
organizations(id,name,ownership_model: 'org_owned' | 'member_owned')profiles.organization_id— nullable, touches no existing RLS policy (auth.uid() = profiles.idkeeps holding for every seat, because every seat is still its own independent profile)billing_accounts(subject_kind: 'user' | 'org', subject_id, provider, external_customer_id)— neverprofilesdirectly, because a Razorpay subscription is not re-assignable between subjectsget_my_entitlements()returning{ subject: {kind:'user'|'org', id}, tier, features, usage }— the one thing worth deciding now even though the RPC itself isn't built: whoever eventually writes it must not scope the signature touser_idalone, or this exact redesign conversation repeats itself when the first org customer signs
One piece is already correctly scheduled, not newly discovered here: get_public_profile_by_slug's RETURNS TABLE gains an organization_id column now, always null, as part of QRS-308 (26.0.1's is_published work, CR-26.0.1-07) — because that RPC is going live in this release, and widening a live RPC's signature later carries requires_min_app_build. Recorded here so the reasoning has one home instead of being re-derived from release.json and this ADR independently.
Related
- Tracker: "No Organization-above-Workspace entity" (
documentation/portal/dev-tracker/tracker.md) - Subscriptions review
- Merchant console review (mobile/desktop workspace divergence)
ui_kits/merchant-console-v2/CONTRACTS.md§2 (Claude Designs project)