Appearance
Tier system
Rewritten 2026-08-08. The previous version of this page described the retired Vite SPA —
src/tiers/,src/routes/index.jsx,SupabaseAuthContext.jsx,ProtectedRoute.jsx,AdminRoute.jsx,digital-menuas the reference feature — as if it were current, with no warning of any kind. All of it now lives inlegacy/and is reference-only. That page was the highest-risk document in the portal: it told a reader the app had a structure it has not had since ADR-0011/0012. Index of truth: current architecture state.
Two stacks, then tiers inside them
A "tier" is still an access boundary, but tiers no longer live in one codebase. ADR-0011 matches each surface to the tool its job demands, so the first question is always which app, and only then which tier.
Only UI is written twice. Data logic — schemas, domain rules, service contracts, entitlement resolution — lives in packages/ and is written once. That is the whole point of the split, and a feature that duplicates data logic across the two apps is a defect, not a shortcut.
Where the code actually lives
apps/mobile/src/
app/ Expo Router routes ONLY — thin, no logic
index.tsx waits for rehydration, then ONE <Redirect> via resolveEntryRoute
(user)/ (tabs)/ · onboarding/ · profile · settings · reminders · create · sign-in
tiers/{user,admin}/features/{name}/{screens,hooks,utils}/ + index.ts + README.md
ui/ the SYSTEMIC RN primitive set + theme/ + motion/
lib/ · stores/ · i18n.ts
apps/web/
app/routes/ React Router v8 file routes
src/tiers/public/features/setu-card/ the manifest-driven renderer
src/tiers/admin/ two placeholder READMEs — not started
legacy/ the retired Vite SPA. REFERENCE ONLY.If a search turns up .jsx, a Vite config, import.meta.env, src/tiers/ at the repo root, or a direct supabase.from() call, you are in legacy/. Read it for prior art, never as the current shape of anything.
Routing and gating — how it actually works now
There are no ProtectedRoute / AdminRoute components. Those were the SPA's mechanism and are in legacy/. Gating is routing-level and differs per stack, deliberately:
Stack 2 · apps/mobile | Stack 1 · apps/web | |
|---|---|---|
| Session store | Zustand (stores/sessionStore.ts), persisted via storage.ts. Not a React Context. | Request-scoped; SSR has no client session singleton |
| Gate | src/app/index.tsx blocks on rehydration, then one <Redirect> from resolveEntryRoute({ hasSeenWelcome, email, onboardingCompleted, accountType }) | Server-side in the route loader — never a client-side role check |
| Never | renders a screen the user should not see, even briefly | trusts anything the browser sent about who the user is |
Admin authorization is server-side in the loader. A client-side role check is a cosmetic gate; the loader plus requireAdmin in the Edge Function is the real one. Both are always required.
Cross-tier import rules [ENFORCED — lint-gated]
- Mobile
user ⇎ admin— neither imports the other. packages/*andtooling/*never import app@/code. Enforced intooling/eslint-config/guardrails.js.- Cross-package imports always go through the
@qrsetu/*workspace name, never a relative path out of a package. - Never branch on archetype, industry or plan in app code — also lint-gated (ADR-0021 D4). Ask for a feature. A screen containing
if (archetype === 'dairy')is the conditional sprawl the whole platform model exists to prevent, and it is the one item here that cannot be retrofitted.
Feature directory structure
Every feature is src/tiers/{tier}/features/{name}/:
screens/ route-level screens, each a DIRECTORY with co-located __tests__/
hooks/ feature data hooks — useQuery/useMutation against a packages/data service
utils/ pure computation
index.ts the barrel — import from here, never deep
README.md MANDATORY (ADR-0012) incl. parity status + the proactive-value answer
remindersis the current reference feature.digital-menuis not — it islegacy/, excluded from R1, and slated for an R2 re-home onto the Goods archetype.
A feature with a merchant half and a public half owns both, in parallel directories across the two apps (apps/mobile/src/tiers/user/features/X + apps/web/src/tiers/public/features/X), with the shared data model, schemas and pure logic in packages/ so it is written once.
See ADR-0011 · ADR-0012 · Data Access Strategy.
Where the code actually lives (operating-manual text)
Provenance — moved from CLAUDE.md on 2026-09-23 (QRS-1288)
This is the verbatim text of CLAUDE.md § "Where the code actually lives (read this before searching)" as of commit 00c1eca, relocated here under the context-architecture programme. Sentences of the form "this said X until [date]" are corrections recorded at the time they were made; the live rule is the corrected one. Retired vocabulary inside those corrections names what was retired and is not a live claim.
Architecture
Where the code actually lives (read this before searching)
apps/mobile/ Stack 2 — the live app. Expo Router + NativeWind. THE merchant product.
apps/web/ Stack 1 — React Router v8 (SSR, framework mode) + Tailwind v3 over @qrsetu/tokens.
A real workspace as of M3 (QRS-307 delta): the public Setu Card route
(`/:slug/setu-card`, with `/:slug` a 301 to it) + its manifest-driven
renderer are built; the landing page
(`/`) is BUILT (26 files under `src/tiers/landing/`) — ⚠ this said "placeholder, not yet
built" until 2026-08-28. The merchant desktop console (`/merchant/*`) and the
marketplace (`/marketplace/*`) are built too; the admin tier is not started.
packages/ schemas → {domain, data}; leaves: tokens · utils · i18n · analytics · observability
tooling/ typescript-config · eslint-config · tailwind-config
supabase/ migrations (all v2) · functions (+ _shared + _archive_pre_v2) ·
tests/database (pgTAP) · config.toml <- counts: measured inventory, top of file
legacy/ the retired Vite SPA — REFERENCE ONLY, not built, not linted, not a workspacelegacy/ is where the old src/tiers/*, SupabaseAuthContext.jsx, ProtectedRoute.jsx, digital-menu, and the ~56 direct from() callers went. Read it for prior art, never as the current shape of anything. If a search turns up .jsx, a Vite config, or import.meta.env, you are in legacy/. Digital Menu specifically is excluded from R1 and slated for an R2 re-home onto the E-commerce archetype (ADR-0009) — do not treat its bespoke schema as the model for a new vertical; the vertical-archetype platform (ADR-0009) is.
A feature with a merchant half and a public half owns both, in parallel directories across the two apps (apps/mobile/src/tiers/user/features/X + apps/web/src/tiers/public/features/X) — with the shared data model, schemas and pure logic in packages/ so it is written once.
Frontend platform — surface-matched, two stacks
Provenance — moved from CLAUDE.md on 2026-09-23 (QRS-1288)
This is the verbatim text of CLAUDE.md § "Frontend platform — surface-matched, two stacks (building from R1 — ADR-0011)" as of commit 00c1eca, relocated here under the context-architecture programme. Sentences of the form "this said X until [date]" are corrections recorded at the time they were made; the live rule is the corrected one. Retired vocabulary inside those corrections names what was retired and is not a live claim.
Frontend platform — surface-matched, two stacks (building from R1 — ADR-0011)
QRSETU matches each surface to the tool its job demands — two frontend stacks over one shared core:
- Stack 1 — React web (React Router v8, DOM, keep shadcn/ui): the public Setu Card (
/:slug/setu-card; ⚠ NOT/b/:slug— that was the retired Vite SPA's form and this line carried it until 2026-08-24, in the same file that lists/b/ /s/ /w/as archived vocabulary) and landing render server-side (SSR) — SEO-critical, correct WhatsApp/social OG previews, JSON-LD, fast first paint, Cloudflare cache (>95%). SEO is the primary growth factor and is never routed through React Native Web. Admin also lives here (same RR8 app, shadcn, desktop-first for dense authoring) with an installable PWA for mobile ops/field-demo (private, behind login — never public stores). (One React framework for all of Stack 1 — solo-dev maintainability; Astro is a later public-only CWV optimization if needed.) - Stack 2 — Universal Expo/React Native: the merchant product app — one RN codebase → Android native (R1), iOS native (R1 — moved up from R2 on 2026-07-26; free Personal Team signing needs no Apple fee to develop), responsive web via RNW. Structural parity holds by construction here (app ≈ PWA ≈ mobile web = same code). Expo Router; NativeWind styling (gluestack-ui candidate; Tamagui alt). Billing is web-first via Razorpay — the native app is a free companion app with no in-app purchase UI/CTA (Apple 3.1.3(d); 0% store commission; upgrades convert via email/web), ADR-0002.
Shared across both: the platform-agnostic TS core (TanStack Query, Supabase, RPC/EF services, Zod, entitlement/ vertical resolvers — no DOM deps) and one design-token package (the single source of truth for the look — see Design System). Only the merchant app is (re)built on RN; shadcn is kept for public + admin.
Repo structure [ENFORCED — ADR-0012, finalized 2026-07-20]: the frontend is a greenfield rebuild in a monorepo (npm workspaces) — apps/{web,mobile} over bounded packages with a one-way, acyclic dependency graph: schemas → domain → data, plus leaves tokens/utils/i18n/analytics/observability, and shared tooling/{typescript-config,eslint-config,tailwind-config}. Data logic is written once (in packages/); only UI is written twice (shadcn/DOM vs RN/NativeWind). The 4 tiers split across apps — landing/public/admin → apps/web, user/merchant → apps/mobile. Feature = presentation + glue only. Server state = TanStack Query; client state = Zustand, per-app. All strings via @qrsetu/i18n t() (English R1). The Supabase backend is unchanged; the retired Vite SPA is reference, not migrated — ⚠ it lives at legacy/, and there is no src/ at the repo root at all; Digital Menu is preserved and re-homed in R2. Full structure, boundaries, conventions, and the README-everywhere standard: ADR-0012.
Mobile app structure (operating-manual text)
Provenance — moved from CLAUDE.md on 2026-09-23 (QRS-1288)
This is the verbatim text of CLAUDE.md § "Mobile app structure (apps/mobile/src)" as of commit 00c1eca, relocated here under the context-architecture programme. Sentences of the form "this said X until [date]" are corrections recorded at the time they were made; the live rule is the corrected one. Retired vocabulary inside those corrections names what was retired and is not a live claim.
Mobile app structure (apps/mobile/src)
File-routed by Expo Router — src/app/ holds routes only (thin), features hold the code:
src/app/index.tsx— waits for session rehydration, thenresolveEntryRoute(...)→ one<Redirect>. Never renders a screen the user shouldn't see.src/app/(user)/…— the merchant tier:catalog,notifications,profile,reminders,settings,sign-in, plusonboarding/{welcome,setup}and(tabs)/{dashboard,chats,leads,more}.+html.tsxis the RNW web shell;parity-probe.tsxis the native parity probe. ⚠ This is a ROUTE list and does not map 1:1 onto features — a distinction this file previously blurred by presenting one list as both.createis a route with no feature directory (create.tsxis a self-described "thin, honest placeholder"EmptyState), andsign-inis a route backed by theauthfeature. Don't go looking forfeatures/sign-in/. ⚠createand(tabs)/leadsare GONE — both routes were deleted when the two placeholder screens were retired, so the paragraph above describedcreate.tsxin the present tense until 2026-08-28. The tier has since gainedbanking,card-activity,card-editor,collections,orders/[id],payments,plan-billing,qr-toolsandthread.src/tiers/{user,admin,consumer}/features/{name}/{screens,hooks,utils}/+index.tsbarrel +README.md. Screens are directories with co-located__tests__/. The feature list and count are in the measured inventory at the top of this file — it said "Eight" until 2026-08-13 and there were eleven, the three uncounted ones (card-editor,orders,qr-tools) being delivered Ganapati-journey work that the operating manual was understating. That direction of drift is worth noting: stale docs usually overstate progress, and here they hid it.remindersis the reference feature — it is the only one carryinghooks/+notifications/+screens/and three__tests__/dirs — thoughcatalog,dashboard,onboarding,profileandsettingsnow have comparable depth.src/tiers/admin/exists but is EMPTY (two READMEs, zero code; its own README says "DEFERRED / not built … leave empty (README only)"). Noteguardrails.jsstill ships atiers/admin/**lint block that currently matches nothing — the boundary is pre-wired, not load-bearing yet.src/ui/— the systemic RN primitive set (design-first, ADR-0015): count in the measured inventory, plustheme/(incl.useCardColors,useThemeColors,useElevation,elevation),motion/, and 36 co-located tests.Button,TextField,Sheet,PressableScale,GradientHeadline,AppText,Calendar,ClockPicker,ConfirmSheet,Toast,Wordmark, … Import from@/ui, never deep.src/lib/(queryClient,colorScheme,observability,avatarPicker) ·src/stores/(Zustand:sessionStore,themeStore,localeStore,storage) ·src/i18n.ts.- Three
src/directories this file omitted until 2026-08-12:src/dev/,src/release/, and — the one that will actually mislead you — a top-levelsrc/features/holding the entitlements hook (useFeatures.ts). ⚠ That is NOTsrc/tiers/user/features/. One repo, one word, two unrelated meanings, at two paths that both read as "features" in an import. This is precisely the collision the feature-scoped-naming rule exists to prevent (primitiveshad three meanings,templateshad two), and it is live in the tree right now — treat it as a naming defect awaiting a rename, not as a convention to copy.
Cross-tier import rules [ENFORCED, lint-gated]: mobile isolation is three-way — user ⇎ admin ⇎ consumer, all six directions, derived from a TIERS array in guardrails.js rather than hand-paired constants. ⚠ This said user ⇎ admin only, until 2026-08-28; packages/* and tooling/* never import app @/ code.
Auth and session (current implementation)
Provenance — moved from CLAUDE.md on 2026-09-23 (QRS-1288)
This is the verbatim text of CLAUDE.md § "Auth & session (current implementation)" as of commit 00c1eca, relocated here under the context-architecture programme. Sentences of the form "this said X until [date]" are corrections recorded at the time they were made; the live rule is the corrected one. Retired vocabulary inside those corrections names what was retired and is not a live claim.
Auth & session (current implementation)
Session state is a Zustand store (apps/mobile/src/stores/sessionStore.ts), persisted via storage.ts — not a React Context. Gating is routing-level: src/app/index.tsx blocks on rehydration and redirects once via resolveEntryRoute({ hasSeenWelcome, email, onboardingCompleted, accountType }). There are no ProtectedRoute/AdminRoute components in the current app (those are legacy/), and role-based admin gating arrives with apps/web (ADR-0006).