Skip to content

Architecture Decision Records ​

This log exists to answer one question raised across the Templates, Subscriptions, and Ad Manager screen reviews: the Claude Designs prototype has, in places, designed further ahead than the real backend has decided. CONTRACTS.md (Track B of the approved plan) already specifies a tenant model; Templates.dc.html already implements a real entitlement + lifecycle + widget-security model; Subscriptions.dc.html already designs proration/dunning/refund UI. None of that is wrong to have designed — but before any of it becomes real schema, Edge Functions, or RPCs, the underlying architecture questions need an explicit decision, not an implicit one inherited from whatever the prototype happened to draw.

Why ADRs, not just tracker entries: a QRS-### tracker row is for a bounded bug/debt/risk item. These questions are wider — each has multiple real options, cross-cutting consequences, and a recommendation that needs product sign-off before any migration or Edge Function work starts. That's what an ADR is for. Each ADR below still cross-links the QRS-### rows that triggered it.

Status lifecycle ​

StatusMeaning
🟡 ProposedDrafted with a recommendation; awaiting product/engineering sign-off
🟢 AcceptedSigned off; the recommendation is the plan of record
🔵 SupersededReplaced by a later ADR (linked)
⚫ RejectedConsidered and explicitly not adopted (reasoning kept for history)

Log ​

#TitleStatusDepends on
ADR-0001Tenancy & identity model (Organization above Workspace)🟡 Proposed—
ADR-0002Subscription & billing system of record (PSP vs in-house)🟢 Accepted — Razorpay confirmed 2026-07-20ADR-0001
ADR-0003Template system scalability (rendering engine, import contract, ad-slot schema)🟢 Accepted, amended 2026-08-04 (JSON blocks only, custom HTML cut)—
ADR-0004Advertising & sponsored placements platform scope🟡 Proposed, amended 2026-08-04 (seam shipped, inert)ADR-0003
ADR-0005Affiliate, referral & growth rewards program (cash commission + viral/gamified growth)🟡 Proposed, revised 2026-07-19ADR-0001, ADR-0002
ADR-0006RBAC & authorization model (platform role vs tenant role, capabilities-as-code)🟢 Accepted (D1–D3 signed off 2026-07-19) · 🟡 amendment A1 Proposed 2026-09-28 (what gets built: ADR-0035)ADR-0001, ADR-0003
ADR-0007Feature entitlements & subscription gating (freemium; domain × tier × feature command center)🟡 Proposed, revised 2026-07-20ADR-0002, ADR-0006
ADR-0008Email & notifications (ZeptoMail transactional + Zoho Campaigns promotional)🟢 Accepted (OTP = email; split confirmed 2026-07-20)—
ADR-0009Business-vertical archetype platform (metadata-driven verticals; new vertical = config, not code)🟡 Proposed (direction endorsed 2026-07-20)ADR-0001, ADR-0007
ADR-0010Analytics & reporting data architecture (command-center read model; canonical metrics + rollups)🟡 Proposed (direction endorsed 2026-07-20)ADR-0009, ADR-0007
ADR-0011Frontend platform architecture (surface-matched: web-DOM for public+admin, universal Expo/RN for the merchant app)🟢 Accepted (hybrid signed off 2026-07-20; SEO uncompromised) · 🟡 amendment of 2026-09-28 Proposed (admin shares the monorepo, not the console app: ADR-0034)ADR-0009
ADR-0012Monorepo structure, package boundaries & documentation standard (npm workspaces; bounded packages; README-everywhere)🟢 Accepted (greenfield rebuild; finalized 2026-07-20)ADR-0011, ADR-0009, ADR-0007
ADR-0014Public data exposure & least-privilege grants (RLS filters rows not columns → column-level grants for anon; every policy names its TO audience)🟢 Accepted, amended 2026-08-04 (template field allow-list bound to this ADR's projection)ADR-0006, ADR-0001
ADR-0015Design governance & drift reconciliation (asymmetric: tokens/primitives design-first, screens code-first; record at divergence, sync at the release boundary)🟢 Accepted (adopted 2026-07-26)ADR-0011, ADR-0012
ADR-0016Reminders domain model (rule + sparse exception rows, occurrences computed not stored; legacy tables archived not dropped after the pre-flight found 11 rows on Prod; the iOS 64-pending cap shapes the model)🟢 Accepted (adopted 2026-07-27)ADR-0007, ADR-0009, ADR-0012, ADR-0014
ADR-0017Cross-platform parity automation (layered by what each layer can observe: hooks + static gate shipped, native probe specified; a green layer is never reported as parity)🟡 Accepted, phased (layers 0–1 adopted 2026-07-27)ADR-0011, ADR-0015
ADR-0019Setu Card rendering, preview, versioning & switching (one DOM renderer; fidelity-preview vs editing-affordance; three versioning locks; lossless switching via T12 + readiness + graceful degradation)🟢 Accepted (adopted 2026-08-04)ADR-0003, ADR-0011, ADR-0015, ADR-0014
ADR-0020Platform schema redesigned from first principles (workspace as primary tenant, solo = N=1; public cards table instead of a private-table projection; one feature registry × 3 axes × 8 scopes — see ADR-0021 for how its defaults are derived; text keys for reference data) — qr-setu-dev is the source of truth and greenfield; the ..._baseline_schema_from_prod squash is retired as a foundation and Dev is re-baselined, not migrated🟡 Proposed — awaiting owner approval (2026-08-07)supersedes parts of ADR-0001, ADR-0007, ADR-0009; amends ADR-0014
ADR-0021Feature entitlement & control plane (8 scopes x 3 axes; applicability DERIVED from primitive composition so grants stay SPARSE - never a 30x40 admin matrix; grant source lets a vendor self-configure; on_exceed makes a downgrade non-destructive; a lint rule bans branching on archetype/industry/plan in app code)🟡 Proposed - awaiting owner approval (2026-08-07)supersedes ADR-0007 storage; depends on ADR-0020, ADR-0009, ADR-0006
ADR-0022Organization resource sharing (resolution over the org as a UNION, never duplication - one row per car, twenty agents, exclusivity holds by construction; per-type sharing flags default OFF; feature grants and RBAC permissions stay distinct) - the car-dealership case, the first planned Enterprise customer🟡 Proposed - awaiting owner approval (2026-08-08)amends ADR-0020, ADR-0001; depends on ADR-0006, ADR-0021
ADR-0023Organization hierarchy, locations & seat licensing (a workspace TREE with a materialized path, not a fixed Org->Location->User hierarchy - a showroom is a business unit, a location is an address; seats license USERS not workspaces - 8 showrooms + 30 agents = 39 cards but 30 seats; sharing walks the ancestor path so siblings stay isolated; roles assignable to a subtree makes "showroom manager" expressible)🟡 Proposed - awaiting owner approval (2026-08-08)extends ADR-0022; amends ADR-0020, ADR-0001
ADR-0024Enterprise operating model: divisions, oversight & assets (sharing flows DOWN, oversight flows UP - two directions, two mechanisms, only one was modelled; the customer belongs to the showroom not to Sales or Service, so the sales-service disconnect is solved by construction; Asset is a tenth primitive - the customer's vehicle, the installed AC, what an AMC is actually against; Brand/Division/Team stay tree nodes rather than fixed levels) - derived from dealership workflows, not from the org chart🟡 Proposed - awaiting owner approval (2026-08-08)extends ADR-0023, ADR-0022
ADR-0025Campaigns, offers & the targeting primitive (five of six pieces already existed; a scheduled campaign is the first RENDER-TIME temporal gate and collides with the edge cache -> scheduled purge via the outbox, not a short TTL; a first-party offer is NOT the inert third-party promo_slot; targeting vocabulary shared with grants but tables kept separate; Campaign is the 11th primitive; loyalty/points unblocked iff ledger.unit is not INR-only)🟡 Proposed - awaiting owner approval (2026-08-08)extends ADR-0022/0023/0024; amends ADR-0004, ADR-0019
ADR-0026Vendor verification & governance (verification gates CAPABILITIES, never signup or publishing - a document wall would destroy anonymous-first for the majority of vendors who need none; verification_status on the workspace plus an append-only event ledger because an audit that can be updated is not an audit, and revoked differs from rejected; ⚠ the gating needs NO NEW MECHANISM - it is an ops-written feature_grants row at scope_kind='workspace', so a fourth axis would have been a regression; required evidence declared PER INDUSTRY as data, defaulting to nothing; the public "verified" badge DEFERRED because a stale trust signal is worse than none; documents are bucket='private' with bounded retention)🟡 Proposed - awaiting owner approval (2026-08-09)depends on ADR-0021, ADR-0014; unblocks jewellery schemes + RERA
ADR-0027Card freshness: purge-on-write, never TTL (three independent requirements demanded render-time dynamism in a single day - scheduled offers, metal-rate pricing, daily published rates - so one answer rather than three local ones; a short TTL is wrong in both directions at once and client-side rendering forfeits Tier 0; ⚠ a VENDOR-TRIGGERED purge is preferred and two of the three qualify, so prefer a manual toggle over an expiry date; purge is at-least-once, enqueued IN THE SAME TRANSACTION as the write, and a failed purge must be visible; a value derived from a mutable input is never snapshotted onto the row - except by a transaction, which records a historical fact)🟡 Proposed - awaiting owner approval (2026-08-09)amends ADR-0019, ADR-0025
ADR-0028URL namespace & audience routing on one origin (five audiences, no subdomain, and a FLAT vendor slug namespace at the root - so every first segment spent on an audience is permanently removed from the vendor namespace and is only safe because it is already reserved; the universal Expo product mounts ONCE at /app via experiments.baseUrl rather than twice, with /merchant + /consumer as 301 vanity entries; ⚠ /admin and /org are different TRUST boundaries and must never share a route group - conflating platform admin with an org admin is privilege escalation; re-implementing merchant onboarding in the DOM was rejected as the QRS-249 duplicate-source class)🟡 Proposed - owner asked for the recommendation and to adopt it (2026-08-19) · amendment A1 Proposed 2026-09-28 (admin moves to admin.qrsetu.com: ADR-0034)constrains ADR-0011, ADR-0019
ADR-0029WhatsApp communication platform via Meta Cloud API, direct — no BSP dependency (one channel-agnostic envelope, one dispatcher choke point, consent fails closed for marketing; QRSETU is the single sender of record in R1 with per-tenant WABAs kept additive; the two Meta gaps named rather than papered over: no billing API so the Admin Hub ships a runbook not a recharge UI, and per-user marketing caps make utility-on-the-money-path the reliable core)🟡 Proposed — direction set by the owner 2026-08-21, awaiting sign-offamends ADR-0008; depends on ADR-0021, ADR-0027, ADR-0028
ADR-0030Tenant communication identity & credit monetization (both goals are achievable but only at one address: a customer's own branding requires a customer-OWNED account — "creating a WABA account for each Client" is contractual, with a 30-day transfer obligation a shared account cannot honour — while QRSETU billing for their traffic requires a Solution Partner credit line attached to it, after which QRSETU is "Bill To Party"; Tech Providers have no credit line, so T3 = their brand + their bill and T4 = their brand + our bill, the resale tier; ⚠ many brands on ONE shared account is REJECTED on four grounds, and this ADR records that it reached that verdict via two wrong turns — first "impersonation" (wrong mechanism, the policy bars it only "without permission"), then "permitted" (over-read a narrow exception: the Display Name Guidelines demand the link be public on both websites); ⚠⚠ and reselling has NO wholesale arbitrage — volume discounts are per-portfolio, monthly-resetting, utility/auth only and never marketing, so volume never pools across customers; credits are the shared Ledger/Balance primitive with a unit code, the FOURTH design waiting on it, blocked on the non-existent Model A collection path; the per-workspace send cap is promoted to an availability control by the portfolio-wide limit; and credit-line attachment is ONE-WAY, so the billing model is a decision with a deadline)🟡 Proposed — raised by the owner 2026-08-22extends ADR-0029; depends on ADR-0021, ADR-0025, ADR-0002
ADR-0031Domain schemas for bounded contexts (a schema is the module, public is its published interface): a bounded context that owns a table set gets its own Postgres schema, core and shared objects stay in public, and the decidable line is "public holds what more than one product surface depends on; a domain schema holds what exactly one product surface owns". ⚠ The axis is the BOUNDED CONTEXT, never the AUDIENCE — consumer_biodata_* was evaluated and rejected as factually wrong on day one, because a biodata is owned by a PERSON and handle_new_user states that primary_context is "PREFERENCE ONLY - never an authorization input", so a merchant can own one for their sister with no schema change; chat is the same trap from the other side, spanning both surfaces. ⚠⚠ Flat prefixes were also measured onto the 63-byte identifier ceiling — consumer_biodata_access_requests_share_id_requester_user_id_key is exactly 63, Postgres truncates SILENTLY, and this repo already sits at 61 with no prefix at all; a schema gives 17 bytes of headroom instead of 0. RPCs stay in public so the client is untouched (measured: zero direct table references, the from() ban honoured perfectly), which also means domain tables are not REST-reachable at all; anon/authenticated get no USAGE, making ownership enforceable rather than merely legible. Contexts are a CLOSED vocabulary (biodata, meetings) and it governs NEW contexts only🟢 Accepted - owner decision 2026-09-04extends the QRS-436 feature-scoped naming rule to the database layer; depends on ADR-0014 (grants), ADR-0012 (boundaries)
ADR-0032Chat and platform identity, locked: "you are reachable because you gave someone a way to reach you, not because they know your number". The phone is a CREDENTIAL and never an address, a key, or a column in public.users (measured: it is not one today); the slug is the address and every account gets one at creation (measured: handle_new_user references it ZERO times, and only 2 of 10 Dev users hold one, so eight accounts are unreachable); businesses are discoverable, people are never; reach is by capability plus a write-only invite whose answer never varies, so a number can be used without becoming an oracle; a first message to a person is a REQUEST and to a business is not, because a card exists to be messaged; and a conversation is between PRINCIPALS. ⚠ Measured while deciding: conversations.workspace_id AND consumer_user_id are both NOT NULL, so consumer-to-consumer and business-to-business are equally unrepresentable and only one of four communication flows has a model; there is no request/accepted state anywhere; and every chat table holds zero rows, which is what makes the change cheap now. ⚠⚠ Mobile-number identity REJECTED on compounding grounds, the sharpest being that it defeats blocking (a new SIM restores reach) and that it would print a phone number on a printed sticker; a minted numeric ID REJECTED as a third public identifier. Instagram’s request folder is ADOPTED, its global people search is not: it gates attention, we gate reachability. Chat stays in public — ADR-0031 applied, not overridden🟢 Accepted - owner decision 2026-09-05amends the consumer plan’s D-v; constrains ADR-0028 (every account consumes an address); depends on ADR-0031 (schema placement), ADR-0006 (RBAC), ADR-0021 (entitlements)
ADR-0033The biodata reading has one renderer, and the app embeds it: the web page as it renders in a phone browser is canonical; the in-app Preview and a recipient's in-app reading show that page (react-native-webview on Android and iOS, an <iframe> on the PWA), with the owner band and tier switch native around it; a scan opens the app through App Links and Universal Links. Public-only content removed (app strip, grow block, footnotes), the support card and the report link kept. Chosen after two approved artboards and two renderers produced two experiences that every per-surface check passed. ⚠ Supersedes the react-native-webview rejection in ADR-0011 and ADR-0019 for the biodata reading only; the Setu Card preview is unchanged. Open: how the page knows the owner (extends sign-in, ADR-0018 first), the Apple Team ID, devv deploy🟢 Accepted - owner decision 2026-09-27supersedes part of ADR-0011 and ADR-0019; depends on ADR-0028 (URL namespace)
ADR-0034The admin origin, its session and its edge gate: admin.qrsetu.com is its own Cloudflare Worker, built from a new apps/admin workspace that is rendered in the browser with no server-rendered data, so there is no @supabase/ssr; six Workers in all, on single-level hostnames that Universal SSL covers, with custom domains bound in the dashboard and --name on deploy; a strict CSP, frame-ancestors 'none', noindex and no-store HTML; ⚠ Cloudflare Access is REQUIRED in front, because the static handbook at /handbook/ cannot be protected by the app's sign-in and because MFA is declined; staff sign in with email and password, and the session belongs to the admin origin, isolated from qrsetu.com where merchant-authored cards render: the script-injection blast radius is the decisive reason; the Operations Handbook lives in apps/admin/handbook/ on a shared VitePress theme. Rejected: /admin on qrsetu.com, one Worker for both hostnames, SSR cookie sessions🟡 Proposed (2026-09-28)amends ADR-0011 and ADR-0028; depends on ADR-0035
ADR-0035Operator identity, permissions and the admin data plane: the proposal's single operator_role graduates to ADR-0006's shape, because the design's time-bound assignments fire the proposal's own trigger (operators, roles, module × action grants with a record-scope qualifier, assignments with starts_at and ends_at); one permission registry written as code in @qrsetu/domain, with the seeds generated from it and none written until the four role models are one; platform staff only; validity evaluated at check time, because there is no scheduler; separation of duties as refusals, with staff management and every Super Admin or Platform Admin grant reserved to a Super Admin, and Super Admin assignments always permanent; ⚠ every admin RPC checks operator_can(auth.uid(), …) itself, called with the caller's JWT, and the same call checks the session is still live (is_session_live()) and inside the staff time-box; the service role is used only for the Auth admin API and Cloudflare; the audit row commits in the same transaction as the act; masked PII with an audited reveal; a directory read model, measured to win at 100,000 users; staff principals are provisioned through a pre-registration row the signup trigger consumes, so a staff account gets no address, and the same row is the pending-invite record; user_metadata never grants anything; invite and reset links carry a token hash so they survive Cloudflare Access; a project-wide password policy; "view as user" becomes a read-only support view inside the panel (ADR-0038, reserved)🟡 Proposed (2026-09-28, revised the same day with the spike results)amends ADR-0006 and the operator proposal; depends on ADR-0031, ADR-0032, ADR-0034
ADR-0036Account holds, and every point where they are enforced: suspend and block write an append-only hold, never a status, with one active hold per kind per subject and the status derived from the holds; ⚠ a hold on a person is a ban plus a session delete, because GoTrue has no sign-out by user id and a ban deletes no session, which an unban then revives; the holder sees one generic state, with no reason and no kind; a lift completes an incomplete suspend before it unbans; a hold on a business is enforced in our layer: every public page under its address shows one neutral state, its writes are refused, and get_my_context stays the one exempt read for its members; ⚠ is_session_live() in every definer helper closes the window PostgREST leaves open until a token expires, at about 30 µs a call, instead of a shorter project-wide jwt_expiry; 61 functions are executable by authenticated, 5 of them writing, counted from the catalog🟡 Proposed (2026-09-28, revised the same day with the spike results)depends on ADR-0035, ADR-0027; a core-entity change under the architecture change protocol
ADR-0037QR setu's own sales pipeline is a separate, operator-only store: dedicated tables, never a merchant's lead rows; the pipeline only, with no WhatsApp sending in the MVP, because no consent, suppression or inbound STOP store exists and the only approved templates are OTP; a refused lost reason recorded and flagged for the future suppression list; ⚠ the Won link is manual and confirmed, never matched by phone (ADR-0032); the admin's eight stages kept apart from the merchant's six; industries from the industries keys🟡 Proposed (2026-09-28)depends on ADR-0035, ADR-0032, ADR-0031

Numbering note: 0013 and 0018 are both absent, deliberately documented rather than silently skipped. 0018 is reserved for the still-unwritten auth architecture ADR — cited by 10+ source files (supabaseClient.ts, authBootstrap.ts, sessionVault.ts, nonce.ts, socialAuth.*.ts, sessionStore.ts, useSocialAuth.ts, the auth feature README) as their rationale, but the file does not exist (found 2026-08-01, still open). 0013 has no recorded claimant — it was never assigned and no source references it; left as a gap rather than backfilled, so a future reader does not mistake a renumber for evidence the ADR once existed and was deleted. 0038 is reserved for the read-only support view inside the Admin Panel, the owner's answer of 2026-09-28 to "view as user" (ADR-0035, Consequences). It is written with its own design round.

How this connects to the rest of the program ​