Skip to content

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-menu as the reference feature — as if it were current, with no warning of any kind. All of it now lives in legacy/ 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/mobileStack 1 · apps/web
Session storeZustand (stores/sessionStore.ts), persisted via storage.ts. Not a React Context.Request-scoped; SSR has no client session singleton
Gatesrc/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
Neverrenders a screen the user should not see, even brieflytrusts 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/* and tooling/* never import app @/ code. Enforced in tooling/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

reminders is the current reference feature. digital-menu is not — it is legacy/, 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 workspace

legacy/ 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, then resolveEntryRoute(...) → one <Redirect>. Never renders a screen the user shouldn't see.
  • src/app/(user)/… — the merchant tier: catalog, notifications, profile, reminders, settings, sign-in, plus onboarding/{welcome,setup} and (tabs)/{dashboard,chats,leads,more}. +html.tsx is the RNW web shell; parity-probe.tsx is 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. create is a route with no feature directory (create.tsx is a self-described "thin, honest placeholder" EmptyState), and sign-in is a route backed by the auth feature. Don't go looking for features/sign-in/. ⚠ create and (tabs)/leads are GONE — both routes were deleted when the two placeholder screens were retired, so the paragraph above described create.tsx in the present tense until 2026-08-28. The tier has since gained banking, card-activity, card-editor, collections, orders/[id], payments, plan-billing, qr-tools and thread.
  • src/tiers/{user,admin,consumer}/features/{name}/{screens,hooks,utils}/ + index.ts barrel + 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. reminders is the reference feature — it is the only one carrying hooks/ + notifications/ + screens/ and three __tests__/ dirs — though catalog, dashboard, onboarding, profile and settings now 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)"). Note guardrails.js still ships a tiers/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, plus theme/ (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-level src/features/ holding the entitlements hook (useFeatures.ts). ⚠ That is NOT src/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 (primitives had three meanings, templates had 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).