Skip to content

Architecture and backend validation: every capability against the real schema ​

Part of car_sales — the dealership operating layer.

⚠ This page exists because a feature catalogue that has not met the schema is a wish list

📘 Owner instruction 2026-08-23: "I don't want a half-cooked, assumption-based dealership feature catalogue that later turns out to conflict with the actual QR Setu backend."

🧮 Every verdict below was checked against the live migration set, not inferred from the product pages. Where the answer is "no table exists", it says so. QRS-855.

The verdict scale ​

Meaning
🟢 SupportedExisting tables and policies carry it. Client work only
🔵 Minor modificationAdditive columns, one new policy, or a projection change. No new concept
🟡 Architectural enhancementNew tables and relationships, inside the existing tenancy model. The model holds; the schema grows
🟠 Significant redesignTouches the tenancy, identity or isolation model itself
🔴 Not recommendedRefused under the current architecture, with a reason

1 · What the platform already provides ​

🧮 Measured, and this is the part that makes the vertical viable at all.

CapabilityState
organizations + workspaces TREE — parent_id, materialized path uuid[], depth cap 6🟢 built, pgTAP-proven
ownership_model — member_owned / org_owned🟢 built
Three RLS helpers — my_workspace_ids, my_oversight_workspace_ids, my_shared_org_ids🟢 built and proven
setu_cards per workspace, org_owned, slug write-once trigger🟢 built
feature_grants — 8 scopes × 3 axes, limit_value, on_exceed🟢 built
industries text key · business_archetypes · process_primitives🟢 built
Reminders — rules + sparse occurrence exceptions, recurrence expansion in @qrsetu/domain🟢 built and live on Dev
Orders / payments / reconciliation — monotonic status, fallback correlation, detect-only reconciler🟢 built and live
Chat🟢 schema shipped 2026-08-11
outbox with a cache.purge topic⚠ table exists, nothing drains it

⚠ And the four gaps that gate almost everything in this vertical

GapConsequence
RBAC is 0% built — no roles, permissions, role_assignments, is_admin()Every persona view, every rollup, every scoped read. The largest single dependency
Five of the seven primitives this vertical declares have ZERO tables — party, schedule, resource, asset, campaignVisitor register, leads, customers, test drives, vehicle history, campaigns
No write path to organizations anywhere in the productThe only INSERT is a test fixture. Group Intelligence, seats and org billing are all downstream of it
No public media URL — mediaUrl.ts fails closed, no bucket provisionedEvery catalogue image on every public card (QRS-852)

2 · The validation matrix ​

#CapabilityVerdictNew tables / entitiesTenant boundary⚠ The real risk
1Organisation Setu Card🟢noneworkspaceMedia URL blocks its content, not its existence
2Employee Setu Cards🟢noneworkspace + userorg_owned must never become member_owned
3Multi-outlet group structure🟢noneorg + subtreeDepth cap 6; a 3-level dealership fits
4Catalogue and offers🔵noneworkspaceget_public_catalogue projects storage_key, not a URL
5Service and insurance reminders🔵link to assetworkspaceNeeds #9 first; the engine itself is live
6Upward oversight rollups🔵nonesubtree, read-only⚠ must exclude member_owned descendants
7Plan limits — cards, logins, storage🔵noneworkspaceon_exceed needs soft-vs-hard semantics (QRS-850)
8Party / customer registry🟡parties, party_identifiers, party_workspace_links⚠ undecided — see §3.1The most consequential open question on this page
9Vehicle assets + service history🟡assets, asset_eventsworkspace, party-linkedOdometer and date recurrence both needed
10Visitor register🟡visitsworkspaceMust be offline-tolerant and faster than a pen
11Leads + follow-up SLA🟡leads, lead_events, sla_policiesworkspace, assignee-scopedSLA clock is derivation → @qrsetu/domain, not SQL
12Test drives🟡schedules, resources, bookingsworkspaceVehicle as a bookable resource
13Targets and performance🟡targets, target_assignmentssubtreeAssignable to a subtree, per ADR-0024
14RBAC and role hierarchy🟡roles, permissions, role_assignmentssubtree-scoped⚠ See §3.2. Nothing above Team Leader works without it
15Card scan analytics🟡interaction_eventsworkspaceBeacon 404s; profile_analytics has no writer
16Per-standee touchpoints🟡qr_touchpointsworkspace + placement⚠ See §3.3. Without it, ₹225 buys printing
17WhatsApp send, templates, opt-in🟡message_templates, opt_ins, message_log, credit ledgerworkspace⚠ Per-workspace send cap is non-deferrable
18Campaigns🟡campaigns, campaign_targetsorg, targeted at a subtree⚠ Needs an outbox drain worker, which does not exist
19Google Business integration🟡external_accounts, reviewsworkspace, one GBP location per outletExternal approval gate; token storage
20Seats and org subscription🟡org_subscriptions, seat_assignmentsorgOrg-level subscription is inexpressible today
21Group Intelligence🟡none of its ownorg subtreeDownstream of 8, 11, 13, 14, 20. Last, by dependency
22Physical standee orders🔵reuse orders with a goods line typeworkspace⚠ orders is marketplace-shaped; a goods COGS is new
23Discount governance🟡discount_approvalsworkspace⚠ Never an OEM-facing view. The CCI order
24F&I attach workflow🟡attach_offersworkspaceWorkflow only. Never the commission
25Dedicated per-tenant deployment🟠none⚠ project, not workspaceSee deployment and isolation
26Cross-dealer benchmarking🟠aggregate store⚠ crosses tenants📘 Would likely make QR Setu a Data Fiduciary (QRS-837)
27Featured marketplace placement🔴——📘 promo_slot fails closed pending compliance_profile; no consumer base (QRS-849)
28OEM DMS integration🔴——📘 D3. No Indian OEM publishes an API; the card makes it unnecessary
29Insurance / finance commission🔴——Licence-gated: MISP appointment, RBI Digital Lending Directions
30Accounting, GST filing, parts inventory🔴——Tally and Marg territory; the OEM owns parts

🧮 Count: 3 🟢 · 5 🔵 · 17 🟡 · 2 🟠 · 4 🔴. 🔎 The shape is the finding. Nothing needs a tenancy redesign — the four-layer model absorbs the whole dealership product as new tables inside an existing boundary. The cost is not architectural risk; it is volume of build.

3 · The six questions the matrix cannot answer in a row ​

3.1 · ⚠ Is a customer one record across tenants, or one per tenant? ​

This is undecided, it is the most consequential item on the page, and it is a privacy question before it is a schema question

A buyer visits three showrooms in one group, and separately visits a dealer on a different QR Setu tenant. Is that one party row or four?

Consequence
One global party, linked to many workspacesThe group view is correct and de-duplicated. ⚠ But dealer A can potentially learn their prospect visited dealer B — a cross-tenant leak of one customer's behaviour to a competitor
One party per workspacePerfect isolation. ⚠ The group view double-counts, and cross-brand consolidation — the moat — degrades to a name-matching exercise

Recommended shape, and it is a third option: parties scoped per organisation, with party_identifiers (phone-primary) unique within the organisation, never globally.

Result
Within a dealer group✅ De-duplicated. Cross-brand consolidation works, which is what the group pays for
Across QR Setu tenants✅ No linkage at all. Two dealer groups can hold the same phone number and never know
Consumer-side identity📘 Separate concern — a registered consumer's own account is users, and orders already carries a nullable buyer reference

⚠ Do not make the phone number globally unique. It is the obvious implementation and it silently creates a cross-tenant join on the most personal identifier in the system.

3.2 · ⚠ RBAC is not one feature; it is the floor under nine personas ​

🧮 No roles, no permissions, no role_assignments, no is_admin() (QRS-803).

RequirementWhy the ordinary implementation is wrong here
Roles assign to a SUBTREE, not a workspace📘 ADR-0023 D4. A Sales Manager over three outlets is one assignment, not three. A per-workspace role table cannot express it without duplication, and duplication is how a revoked role survives in one row
Oversight is READ-only and org_owned-onlyAn ancestor may read descendants; ⚠ never an employee's member_owned business
Policies must not grant by ROLE alone📘 authenticated will become the logged-in general public once consumers ship. Every merchant-data policy scopes by relationship, never using (true) TO authenticated
The client gate is an affordance, not enforcement📘 The Edge Function is the primary write-enforcement layer; RLS is defence in depth. Both always

⚠ Scalability note that will not surface until there is data

🔎 A subtree predicate calling my_oversight_workspace_ids() per row will dominate every query on leads and interaction_events — the two largest tables this vertical creates. The materialized path uuid[] makes a subtree test indexable (path @> ancestor_path with a GIN index), which is why path exists. The helper must be STABLE and the policy must compare against the indexed path, not re-derive the set per row. Get this wrong and it works perfectly at 500 rows and collapses at 500,000.

3.3 · Per-standee touchpoints — the entity that makes ₹225 defensible ​

text
qr_touchpoints
├── workspace_id          the outlet
├── code                  unique, printed on the standee
├── placement             reception | waiting | table | vehicle | desk | test_drive_bay
├── label                 "Table 4", "Creta display"
├── target                org card | a specific catalogue item
└── active                so a damaged standee can be retired without losing its history

🔎 A scan resolves code → touchpoint → workspace → card, and the interaction event carries the touchpoint_id. That is what turns "someone scanned" into "the waiting-area standee produced 6 enquiries". ⚠ Without this table the standee is a printed sticker and the ₹225 has no argument behind it.

📘 Note the attribution consequence: a standee scan has no employee, so interaction_events.employee_id must be nullable and the outlet becomes the attributed party. A schema that requires an employee on every interaction cannot record the highest-volume interaction type in the product.

3.4 · Campaigns need a drain worker that does not exist ​

🧮 public.outbox exists with a cache.purge topic and nothing consumes it — no worker, no cron, no subscriber. So enqueuing a campaign send today writes rows nobody processes.

⚠ 📘 And a campaign is the first render-time temporal gate in the platform. Every prior decision kept those away from rendering because they break the edge cache. The answer is scheduled purge at campaign boundaries via the outbox — never a short TTL, never client-side. So the drain worker is a prerequisite for campaigns and for card freshness at once, which is an argument for building it once, properly.

3.5 · Audit and logging ​

RequirementState
Structured Edge Function logs → Supabase/Logflare via console.log🟢 live. ⚠ There is no SQL log table by design — the previous one was dropped and its helpers still compile
Error and crash reporting via the @qrsetu/observability seam🟢 live, PII-scrubbed
Tenant-scoped audit trail — who changed a role, a target, a card✅ CORRECTED 2026-08-23: public.audit_log EXISTS since 20260808200000_v2_audit — actor_user_id on delete SET NULL plus actor_role denormalised, before/after jsonb, a constrained action, scope_kind/scope_id, request_id, ip_hash. ⚠ My "does not exist" was a prefix-grep false negative: the table is audit_log, not audit (QRS-869)
Immutable payment event log🟢 payment_events, append-only, unique on (provider, provider_event_id)

⚠ So the narrowed gap is not the table, it is the WRITERS. audit_log can record a role change today; nothing produces one, because roles do not exist. 🔎 That is a better position than it sounded: the schema decision is already taken and correctly, and what remains is emitting rows from the Edge Functions that will own those writes. An org admin editing roles and scopes with no audit row is still the gap an enterprise security review asks about first — it is now a wiring job rather than a design one.

3.6 · Performance envelope ​

ConcernPosition
Composite reads for multi-section screens📘 One composite RPC per screen. ⚠ Three or more parallel read RPCs saturate the connection pool
The largest tables this vertical createsinteraction_events, then leads. Both are subtree-queried, so index the materialized path
Public card render📘 Purge-on-write, never TTL. Cache-hit target >95%
Group rollups across 15 outlets🔎 A rollup over a subtree with 15 leaves is small. The risk is not row count, it is the per-row policy function — see §3.2
Media⚠ The only line that scales without bound. Metered as a soft limit

4 · What this means for sequencing ​

The one-line summary a developer should leave with

🔎 No dealership capability in this vertical requires a tenancy redesign. The four-layer model plus the workspace tree absorbs all of it as new tables inside an existing boundary. Three things are genuinely undecided and all three are on this page: how a customer is identified across tenants (§3.1), whether the subtree policy is indexable in practice (§3.2), and whether dedicated deployment is a product or a promise (deployment and isolation).