Skip to content

Personas, authentication, RBAC and operational capability ​

Part of car_sales — the dealership operating layer. This page defines the complete persona → authentication → RBAC → capability model, and it must be settled before implementation proceeds.

Scope split with persona feature map — read this before adding to either page

AnswersOwns
This pageIdentity. Authentication, RBAC scope, trust boundaries, login routing, must-never-seeThe persona list and their scopes
Persona feature mapNavigation. Per-persona feature trees, plan labels, data flowThe navigation trees and the feature-to-plan matrix

📘 Two pages describing one subject is how QRS-843 happened. On identity this page wins; on navigation that one does. Add a persona here first, then map its navigation there.

1 · The full picture ​

The one idea this diagram encodes

Persona and scope are two independent axes. Persona is the role — what you may do. Scope is the workspace subtree — where you may do it. A Sales Manager at Thane and a Sales Manager at Andheri are the same role at different scopes; a regional manager is one role over several scopes. 📘 ADR-0023 D4 makes roles assignable to a subtree, which is what lets one model serve all three without a per-outlet special case.

2 · Persona identity and context ​

PersonaWho they areHow they log inContext they belong to
Dealer Principal / OwnerOwns the business. Often owns several brandsSame single loginThe organisation root. Oversight over every descendant
General ManagerRuns one outlet, or a regionSame single loginAn outlet node, or a region node above several outlets
Sales ManagerRuns sales at one or more outletsSame single loginThe sales division of one or more outlets
Team LeaderRuns a team of repsSame single loginA team scope inside a sales division
Sales RepresentativeCustomer-facing. Holds a cardSame single loginTheir own agent workspace, org-owned
Receptionist / Front deskLogs every walk-inSame single loginOne outlet, narrow capability
MarketingRuns campaignsSame single loginOrganisation or brand scope
Service AdvisorAftersalesSame single loginThe service division of one outlet
Org AdminThe customer's own administrator — often the DP or their IT personSame login, lands on /orgThe organisation. Not a QR Setu employee
QR Setu Platform AdminOur staffSame login, plus a step-up, lands on /adminOutside every tenant. A different trust boundary
Customer / ProspectThe buyerNo account required. Anonymous-firstNone. Zero workspace memberships

Two of these rows are not like the others

📘 ADR-0028: /admin and /org are different trust boundaries and must never share a route group. Platform admin is QR Setu staff over all tenants; org admin is a customer's employee scoped to one organisation subtree. CLAUDE.md names conflating them as privilege escalation. Siblings with separate layouts and separate guards, never /admin/org/….

And the customer row is the one most easily got wrong: a public card must be fully usable with no account. Registration is demanded only where identity is genuinely required.

3 · What each persona may see, create and must never see ​

PersonaSeesCreates / managesMust NOT see
Sales RepOwn card analytics, own leads, own tasks, own test drives, the outlet's catalogueOwn leads' updates, follow-up outcomes, test-drive bookings, feedback captureA sibling rep's leads or customers. Team or outlet aggregates. Anyone's targets but their own
Team LeaderEvery rep in the team: activity, leads, overdue follow-ups, conversions. Team totalsLead reassignment inside the team, task assignment, team targetsOther teams. Other outlets. Financials above their scope
Sales ManagerAll teams in their division and outlet. Funnel by stage and loss reasonTeam structure, targets, reassignment across teams in scopeOther outlets not in scope. Group-level P&L
General ManagerThe whole outlet, both divisions, per-person performanceOutlet configuration, targets, staff roles within the outletOther outlets unless scoped. Subscription and billing
Dealer PrincipalEvery outlet, every brand, side by side. All personnel performanceGroup targets, outlet creation requests, brand structureNothing operational is hidden. But see the privacy rule below
ReceptionistToday's visitor list, who is available, the catalogueVisitor entries and the initial assignmentLeads not derived from their entries. Any performance data. Any financials
MarketingCampaign performance, audience counts, aggregate funnelCampaigns, templates, offersIndividual customer records beyond campaign need. Per-rep performance
Service AdvisorService bookings, vehicle history, own job assignmentsBookings, service feedbackSales leads and sales performance
Org AdminOrganisation structure, seats, roles, subscriptionUsers, roles, outlet nodes, planOperational customer data they have no role for. Administration is not a licence to read
Platform AdminTenant metadata, health, support diagnosticsPlatform configuration⚠ A customer's operational data by default. Access requires an explicit, time-boxed, audited action

The privacy rule that binds even the Dealer Principal

📘 ADR-0024 D1 plus QRS-386: oversight reaches only org_owned descendants. An employee may run a personal side business or hold a consumer identity in the same platform. Their employer must never see either. This is not a preference; it is the reason ownership_model exists as a column, and pgTAP must prove the negative.

4 · Workflows simplified, per persona ​

PersonaWhat they do todayWhat QR Setu changesHow it rolls up
ReceptionistWrites walk-ins in a notebookTwo fields and a dropdown; assignment happens automaticallyWalk-in volume becomes the outlet's first honest traffic number
Sales RepHands over a phone number; tracks follow-ups from memoryShares a card; leads arrive attributed; tasks carry an SLAEvery interaction becomes their own performance record
Team LeaderAsks each rep what happened; maintains a spreadsheetReads one list: pending, overdue, who has not actedTeam totals aggregate without a report being written
Sales ManagerReconciles team spreadsheetsFunnel by stage and loss reason across teamsOutlet view assembles from the same rows
General ManagerRequests reportsReads the outlet liveFeeds the group view
Dealer PrincipalReconciles brand-by-brand at month end, by handOne screen across every outlet and brandThis is the top of the rollup
MarketingSends campaigns with no attribution backCampaign triggers tied to operational events; response returns as leadsCampaign effect visible beside rep performance
Service AdvisorCalls whoever is rememberedService-due list generated per vehicleAftersales activity joins the same customer record

5 · Login routes: the assessment ​

Recommendation: ONE authentication entry, with context resolved after authentication. Not per-persona, and never per-branch or per-outlet.

Why persona, branch or outlet login routes fail ​

#Reason
1A login route cannot know who you are. Before authentication the system does not know whether this email is a Dealer Principal, a rep or a consumer. Persona routes force the user to self-select and guess
2It leaks identity. Distinct routes and distinct errors let anyone enumerate which email belongs to which organisation and role. "This email is not a dealer admin" is a disclosure
3The routing table becomes O(customers). Persona routes are O(personas) and survivable. Branch or outlet routes scale with the number of outlets sold to. Unshippable
4It hard-codes a tree level — exactly what 📘 ADR-0024 D5 refuses. Some groups run location-P&L, others division-P&L. /outlet/login imposes one organising principle on customers who legitimately differ
5N routes means N auth implementations. Rate limiting, lockout, OTP, session issuance, redirect validation — duplicated until one drifts and becomes the weak one. 📘 This project has had four auth incidents already (QRS-261/273/276/285)
6It breaks on multi-membership. A regional manager over three outlets, or an owner of two groups, needs a selection screen anyway — so persona routes build the context switcher and the route split

One correction to a common framing ​

A principal maps to memberships — plural, workspace-scoped — never to one organisation. CLAUDE.md's three-category rule is explicit: a consumer has zero memberships, and "any resolver, RPC or policy that requires a workspace to answer a question cannot serve category 3 — that is a design defect, not an edge case." The resolver returns a set and must tolerate the empty set.

The shape ​

One authentication endpoint does not mean one post-auth surface

Same login form and same credential store: correct. Same session granting both /admin and a tenant surface: no. Platform-admin access requires a step-up, and /admin is a separate route group with its own guard. Being staff is not a role carried into a tenant.

Should the workspace appear in the URL? ​

Yes — optional, validated, and never authoritative. Recommend /app/w/:workspaceId/... inside the existing single /app mount, so 📘 ADR-0028's "mounted once" decision is untouched.

WhyA Sales Manager will bookmark "Thane sales dashboard". With session-only context that bookmark is ambiguous, and "I sent my colleague a link and he saw his own data" becomes a support burden. It also makes which tenant am I in visible during support
The rule that makes it safeThe client is never the authority. Every request re-validates the workspace against membership server-side. A client-supplied workspace_id that is not yours is the classic multi-tenant IDOR
Disclosure posture404, not 403, for a workspace you are not a member of. A 403 confirms it exists
SwitchingExplicit, never inferred from last-used. Stamped server-side, and audited

Security rules that must hold whatever the routing ​

  1. No policy grants by role alone. Every merchant-data policy scopes by relationship: workspace_id in (select workspace_id from workspace_members where user_id = auth.uid()). Never using (true) TO authenticated.
  2. ⚠ Consumers change what authenticated means. Once consumers log in through the same door, authenticated becomes the logged-in general public — the largest population on the platform. Centralized login is safe only under rule 1.
  3. One composite context call, not three parallel reads. 📘 The connection-pool rule applies exactly here, on the hottest path in the product.
  4. The resolver stays a pure function. 📘 resolveEntryRoute is already pure and unit-tested with node --test. Extend it; do not replace it with hooks and a router.
  5. Staff access to tenant data is an explicit, time-boxed, audited act — never an ambient capability of being staff. Under DPDP we are the processor and the dealer the fiduciary; unaudited impersonation is a breach-notification problem, not a convenience.

The one apparent counter-argument, answered ​

An enterprise buyer will ask for a login on their own domain. That is per-tenant branding, not per-tenant routing — a vanity domain resolving to the same endpoint with a tenant-resolved theme. It does not justify a second authentication path.

6 · What exists today ​

🧮 Measured 2026-08-22.

Status
Single login, email OTP + Google, in apps/mobile🟢 exists
resolveEntryRoute — pure, unit-tested, with a consumer branch🟢 exists
account_type server-derived via the context RPC, not device-local🟢 fixed
Workspace tree, materialized path, depth cap, three RLS helpers, pgTAP negatives🟢 built
login / signup / admin / org reserved as first segments🟢 done
RBAC — roles, permissions, role_assignments, subtree assignment🔴 0% built. 📘 ADR-0006 accepted, unimplemented
is_admin()🔴 zero definitions in the live migration set (QRS-803)
/org and /admin surfaces🔴 apps/web has no auth at all
Tier-boundary lint⚠ covers apps/mobile only. apps/web/src/tiers/admin/** has no guardrail, and no org tier exists to guard

The risk is erosion, not disagreement

Nobody will argue against the /admin ↔ /org boundary. But when /org is finally built under deadline pressure, the cheapest path will be to reuse the admin layout and its guard, because that code will already exist. That is how two trust boundaries become one.

🔎 And note which surface is higher-consequence: a bug on /admin exposes our own platform data to our own staff; a bug on /org exposes one customer's data to another customer. 📘 This repo has a documented guard-asymmetry pattern (QRS-668) where obviously-sensitive paths got hardened and the rest were left bare. /org needs the pgTAP proofs: an org admin cannot read outside their subtree at any depth, and cannot read a member's member_owned workspace.

Cheapest enforcement, all repo-native: extend guardrails.js with apps/web/src/tiers/admin/** ⇎ apps/web/src/tiers/org/** with the org tier rather than after it; assert no /admin path resolves without is_admin() and no /org path resolves a workspace outside the caller's subtree; and pgTAP for the negatives, since none of them is caught by a functional test. Tracked as QRS-842 and QRS-843.