Appearance
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
| Answers | Owns | |
|---|---|---|
| This page | Identity. Authentication, RBAC scope, trust boundaries, login routing, must-never-see | The persona list and their scopes |
| Persona feature map | Navigation. Per-persona feature trees, plan labels, data flow | The 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
| Persona | Who they are | How they log in | Context they belong to |
|---|---|---|---|
| Dealer Principal / Owner | Owns the business. Often owns several brands | Same single login | The organisation root. Oversight over every descendant |
| General Manager | Runs one outlet, or a region | Same single login | An outlet node, or a region node above several outlets |
| Sales Manager | Runs sales at one or more outlets | Same single login | The sales division of one or more outlets |
| Team Leader | Runs a team of reps | Same single login | A team scope inside a sales division |
| Sales Representative | Customer-facing. Holds a card | Same single login | Their own agent workspace, org-owned |
| Receptionist / Front desk | Logs every walk-in | Same single login | One outlet, narrow capability |
| Marketing | Runs campaigns | Same single login | Organisation or brand scope |
| Service Advisor | Aftersales | Same single login | The service division of one outlet |
| Org Admin | The customer's own administrator — often the DP or their IT person | Same login, lands on /org | The organisation. Not a QR Setu employee |
| QR Setu Platform Admin | Our staff | Same login, plus a step-up, lands on /admin | Outside every tenant. A different trust boundary |
| Customer / Prospect | The buyer | No account required. Anonymous-first | None. 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
| Persona | Sees | Creates / manages | Must NOT see |
|---|---|---|---|
| Sales Rep | Own card analytics, own leads, own tasks, own test drives, the outlet's catalogue | Own leads' updates, follow-up outcomes, test-drive bookings, feedback capture | A sibling rep's leads or customers. Team or outlet aggregates. Anyone's targets but their own |
| Team Leader | Every rep in the team: activity, leads, overdue follow-ups, conversions. Team totals | Lead reassignment inside the team, task assignment, team targets | Other teams. Other outlets. Financials above their scope |
| Sales Manager | All teams in their division and outlet. Funnel by stage and loss reason | Team structure, targets, reassignment across teams in scope | Other outlets not in scope. Group-level P&L |
| General Manager | The whole outlet, both divisions, per-person performance | Outlet configuration, targets, staff roles within the outlet | Other outlets unless scoped. Subscription and billing |
| Dealer Principal | Every outlet, every brand, side by side. All personnel performance | Group targets, outlet creation requests, brand structure | Nothing operational is hidden. But see the privacy rule below |
| Receptionist | Today's visitor list, who is available, the catalogue | Visitor entries and the initial assignment | Leads not derived from their entries. Any performance data. Any financials |
| Marketing | Campaign performance, audience counts, aggregate funnel | Campaigns, templates, offers | Individual customer records beyond campaign need. Per-rep performance |
| Service Advisor | Service bookings, vehicle history, own job assignments | Bookings, service feedback | Sales leads and sales performance |
| Org Admin | Organisation structure, seats, roles, subscription | Users, roles, outlet nodes, plan | Operational customer data they have no role for. Administration is not a licence to read |
| Platform Admin | Tenant metadata, health, support diagnostics | Platform 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
| Persona | What they do today | What QR Setu changes | How it rolls up |
|---|---|---|---|
| Receptionist | Writes walk-ins in a notebook | Two fields and a dropdown; assignment happens automatically | Walk-in volume becomes the outlet's first honest traffic number |
| Sales Rep | Hands over a phone number; tracks follow-ups from memory | Shares a card; leads arrive attributed; tasks carry an SLA | Every interaction becomes their own performance record |
| Team Leader | Asks each rep what happened; maintains a spreadsheet | Reads one list: pending, overdue, who has not acted | Team totals aggregate without a report being written |
| Sales Manager | Reconciles team spreadsheets | Funnel by stage and loss reason across teams | Outlet view assembles from the same rows |
| General Manager | Requests reports | Reads the outlet live | Feeds the group view |
| Dealer Principal | Reconciles brand-by-brand at month end, by hand | One screen across every outlet and brand | This is the top of the rollup |
| Marketing | Sends campaigns with no attribution back | Campaign triggers tied to operational events; response returns as leads | Campaign effect visible beside rep performance |
| Service Advisor | Calls whoever is remembered | Service-due list generated per vehicle | Aftersales 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 |
|---|---|
| 1 | A 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 |
| 2 | It 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 |
| 3 | The 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 |
| 4 | It 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 |
| 5 | N 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) |
| 6 | It 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.
| Why | A 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 safe | The 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 posture | 404, not 403, for a workspace you are not a member of. A 403 confirms it exists |
| Switching | Explicit, never inferred from last-used. Stamped server-side, and audited |
Security rules that must hold whatever the routing
- 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()). Neverusing (true) TO authenticated. - ⚠ Consumers change what
authenticatedmeans. Once consumers log in through the same door,authenticatedbecomes the logged-in general public — the largest population on the platform. Centralized login is safe only under rule 1. - One composite context call, not three parallel reads. 📘 The connection-pool rule applies exactly here, on the hottest path in the product.
- The resolver stays a pure function. 📘
resolveEntryRouteis already pure and unit-tested withnode --test. Extend it; do not replace it with hooks and a router. - 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.
Related
- Operating model — the tree, oversight, and card ownership
- Product decisions — capability scope per persona
- Visual product map — the hierarchy diagram
- 📘 ADR-0028 · ADR-0006 · ADR-0023