Appearance
Consumer build — end-to-end implementation plan (client + backend)
APPROVED BY THE OWNER 2026-09-04 — THIS PAGE IS THE PLAN OF RECORD
The owner approved this plan in full and asked for end-to-end implementation and delivery of the consumer side, with a report at every switch between client work and backend work. The recommended defaults in §12 are adopted unless the owner overrides one at the phase that needs it. It supersedes the morning draft of this page and the twelve waves in release-reconciliation.
Context
The owner asked for one plan that covers the entire Consumer side of QR Setu, reconciled against the latest Claude Design prototype and the current repository, precise enough that implementation can proceed without missed screens, architectural drift or rework. This page is the plan of record; every row below becomes a parity-contract row or a tracker id as its phase starts.
How this was produced (so every claim can be checked). The design was re-pulled live from the prototype project (633dc069-6df8-4408-b625-068907c60c33) on 2026-09-04: SCREENS.md, consumer.prompt.md, biodata.spec.md, every consumer artboard and module, the two public biodata and intro card pages, platform/meeting-providers.js. The repo was measured by four independent inventories (mobile tier, backend, shared packages and web, the design files) plus three read-only gates (check:screens, check:design-parity, the conformance ledger). Where the design and the repo disagree, both facts are stated. Where something could not be measured it is marked unverified.
Owner constraints carried unchanged. WhatsApp OTP on Meta only, no third-party messaging or auth provider · no assumed integration or dependency (stop and ask) · no effort estimates · consumer data never indexed · organisation developer accounts only · designs always fetched, never cached · splash intact · no tokens in output · existing-account recognition post-OTP · default theme light · marketplace OFF at launch (D-a) and OFF means ABSENT · Orders is being replaced by Meetings, build nothing new on Orders.
0 · Baseline, measured 2026-09-04
| Fact | Measured value | Consequence |
|---|---|---|
| Design registry | SCREENS.md round 40 (Home), 39 (bar restored), 38 (biodata audit closed), 35 (Meetings) | The plan enumerates from this registry, not from the 2026-08-13 transcription |
| Consumer artboards in the design | 25 in prototype/consumer/ (+ BiodataPage, the intro-card public-page artboard in setu-card/, 3 documents in my-qrsetu/) | Section 1 lists every one with a disposition |
Screen ledger (check:screens) | 39 approved · 29 built · 1 stale · 2 unimplemented · consumer section: 11 rows, transcribed 2026-08-13 | ⚠ The ledger is green and wrong: it has no row for MyQRSetu, Biodata, BiodataView, Meetings, MyProgress, intro card, the four makers or EmblemTrial. Re-transcription is a P0 item (R24) |
| Home parity contract | consumer_home 21 / 39 pass · 10 gap · 8 blocked · 0 route-verified (pull 2026-09-03) | Home implements the retired round-16 stack (QRS-1006) |
| Consumer routes | 10 files under src/app/consumer/, flat Stack inside AppLockGate; no tab bar, no centre button, no /consumer/scan | The chrome is new development, not wiring |
| Consumer features | 6 (account · browse · chat · collection · home · notifications), no hooks/ anywhere, 9 test files / 79 tests | Every screen calls @qrsetu/data singletons inline |
| Data seams | 27 seams · 13 Supabase-backed. Consumer-relevant: consumer REAL · auth REAL · onboarding REAL · chat STUB (reads real RPC shapes, writes stub) · collection · consumerActivity · consumerPrefs · account STUB | Four consumer seams have no backend behind them |
| Backend for consumers | Tables: users, media, conversations/messages/*, feature_grants, orders.buyer_user_id, slugs.user_id. None for biodata, meetings, notifications, push, rate limits, saved, progress, makers. RPCs: get_my_context, get_my_slug, resolve_slug_status (authenticated only), get_my_conversations, get_conversation_messages, 5 anon feed RPCs. EFs: manage-account (update_email · change_password · delete_account (HARD) · claim_slug), manage-media (workspace-only), send-auth-otp, whatsapp-webhook. No chat EF, no consumer upload path, no scheduler, no outbox drain, no push | Sections 3 and 4 name every gap by object |
anon/authenticated table privileges | Zero (asserted by pgTAP). RLS is defence in depth; RPC + EF is the only access path | Every consumer read is an RPC, every write an EF |
| Packages | @qrsetu/domain: consumer/*, chat/*, auth/* exist; no biodata, meetings, scan, progress. @qrsetu/schemas: no consumer schema. @qrsetu/i18n: 8 consumer namespaces, none for biodata / meetings / makers / progress | The design's pure modules are ports still to be written |
| Native deps present | expo-camera, expo-notifications, expo-local-authentication, expo-image-picker, expo-image-manipulator, expo-secure-store, react-native-svg, qrcode | Absent: expo-clipboard, expo-sharing, react-native-view-shot, react-native-webview (each needs a size callout before install) |
| Deep links | scheme: qrsetu only; no intentFilters, no associatedDomains, no assetlinks.json, no AASA | A scanned qrsetu.com/... opens the browser today |
| Analytics | 18 typed events; sinks only in apps/web (GA4; card beacon to an archived EF, QRS-734); no mobile sink | Instrumentation ships, delivery is a decision |
| Release record | releases/26.0.1/release.json: status scoped, target date 2026-08-15 (passed), 121 change records, 0 builds, 0 approvals | The consumer release needs its own record or a re-based 26.0.1 |
| Open tracker rows ≥ QRS-900 | 57, of which these bind this plan: 908/981 keystore · 909 hard delete · 914 person slug resolves as shop · 917 account type never reaches server · 920 Dev media transforms · 921 no rate limiting · 923 watermark · 924 Marathi review · 936 phone + prefix · 952 no SMS channel · 986 slug oracle · 990/1006 Home · 1007 retired name · 1008 scan dead link · 1009 UPI maker · 1010 PDF export · 1011 read state · 1012 media outlive account · 1013 Meta templates · 1014 Zoom | Each is referenced where it bites |
Dispositions used throughout: LAUNCH (in the consumer release) · HONEST-EMPTY (ships with the design's own empty state and no producer) · OFF (marketplace, absent by capability) · LATER (post-release phase P9, named) · SUPERSEDED (design says do not implement). Work classes: FIT (current architecture, wire only) · EXT (extend an existing component, named) · NEW (a component that does not exist) · FUTURE (design it now, build later).
1 · The complete consumer surface, from the live design
Every artboard and module in prototype/consumer/, prototype/my-qrsetu/, the two consumer public pages in prototype/setu-card/, and the platform modules the consumer screens import.
| # | Design file | Round | Designed states (verbatim prop enums) | Repo today | Disposition |
|---|---|---|---|---|---|
| S1 | ConsumerHome.dc.html | 40 | homeState: First run no location · Located anonymous · Registered nothing published · Registered no activity · Registered with activity · Launch variant one industry live · Loading · No businesses in this area · Error · marketplace: Follow platform control · On · Off · tab: home · saved · orders · account. 14 sections, 11 sheets | Built on the retired round-16 stack; 21/39 rows | LAUNCH, rebuild (F2) |
| S2 | MyQRSetu.dc.html | 35, 39 | Open · In discussion · Concluded · Nothing activated · Address just claimed · Loading · Error | Not built | LAUNCH (F3) |
| S3 | Biodata.dc.html | 36, 38 | view content · people; contentState ×10 (Empty and starting · Partly complete · Complete · Editing a live profile · Validation on a required field · Saving · Saved · Loading · Could not save · Read only for a co-manager); accessState ×8 (Nobody yet · A basic link shared and opened · A request waiting · Released people with expiry dates · An expiry approaching · A release withdrawn · A subject removal request · Concluded, ready to reopen); subject Sister · Myself. 13 sheets | Not built | LAUNCH (F4, F5) |
| S4 | BiodataView.dc.html | 39 | viewer released · basic · owner; screenState Ready · Loading · Network error · Link withdrawn · Search concluded; familyLayout from profile · tree · vine · cards · list; subject ×2 | Not built | LAUNCH (F6) |
| S5 | setu-card/BiodataPage.dc.html | 38 | view Recipient page · WhatsApp link preview; scenario ×9 (Basic one cropped photograph · Basic text only · Released photographs · Request submitted and pending · Link expired · Link withdrawn · Concluded · Removal requested (subject view) · Loading); profileStatus Open · In discussion; devicePrompt auto · iPhone · Android; subject ×2; familyLayout ×4 | Not built (no consumer route in apps/web) | LAUNCH (F7, web) |
| S6 | Account.dc.html | 35 | accountState Registered · Guest · Loading · Error; view hub · details · areas · notifs · appearance · privacy · help · about | hub + notifs built against a stub; other views are rows | LAUNCH (F11); areas OFF |
| S7 | Notifications.dc.html | 35 | demoState Default · Empty | Built against a stub; read state per device | LAUNCH (F10) |
| S8 | ScanVerify.dc.html | 35 | demoCode Scanning + 9 payloads (shop sticker · that shop's UPI · unknown UPI · cancelled sticker · lookalike · closed account · ordinary website · unsecured pay page · plain text); cameraState Allowed · Denied | Not built; the ledger's one missing row; /consumer/scan is a dead href (QRS-1008) | LAUNCH (F9) |
| S9 | Meetings.dc.html | 35 | view Calendar · Meeting · New meeting · Meeting account; connection Not connected · Authorizing · Connected basic · Connected paid · Could not connect; scenario Default · Nothing scheduled · Loading · Error · Offline; notification None · New invitation · 15 minutes before | Not built; zero hits for "meetings" in the tier | LAUNCH participant half, HONEST-EMPTY; hosting LATER (F12, C1) |
| S10 | Chats.dc.html | 23, 24 | view list · thread; listState Default · No conversations yet · No custom lists yet · Six custom lists; threadState Default · New from an item · Waiting for first reply · Closed now away reply · Offline · Failed message · Registration required · Blocked; mediaState Default · Uploading · Upload failed · Waiting for connection · Not downloaded yet · Microphone blocked | List built; reads real; every write stub, no chat EF | LAUNCH text chat; media/voice/forward LATER (F16, D-m) |
| S11 | OrderCode.dc.html | 35 | account The buyer · The relative | Built, route gated off (QRS-708); ledger says built and reachable | OFF (marketplace) (F17) |
| S12 | WifiQR.dc.html | 40 | none (theme only) | Not built | LAUNCH (F13) |
| S13 | LinkQR.dc.html | 40 | none | Not built | LAUNCH static, dynamic re-pointing LATER (F13, D-s) |
| S14 | UpiQR.dc.html | 40 | none | Not built | LATER, decision D-d / QRS-1009 |
| S15 | PhotosQR.dc.html | 40 | none | Not built; needs an album page and media | LATER (F13) |
| S16 | the intro-card editor and public-page artboards (their filenames carry the retired name, QRS-1007) | 40 | none (theme) | Not built; revives the retired product name (QRS-1007) | LATER, decision D-l (F14) |
| S17 | MyProgress.dc.html | 40 | scenario Default · First use · Nothing logged yet · Not shared · Loading | Not built; no entry point in CONSUMER_TABS, ACCOUNT_GROUPS or the More sheet; the coach read lives on a merchant screen | LATER, decision D-j (F15) |
| S18 | Featured.dc.html | 22 | props cards · heading · hideHeading · cardWidth · intervalMs | Built (FeaturedBlock.tsx) | OFF at launch (mounted only by vendor view) |
| S19–S22 | ItemFeed · VendorFeed · ItemView · VendorView | 16–22 | as documented in the registry | Built against real anon RPCs | OFF (marketplace), kept reachable from the registry only |
| S23 | EmblemTrial.dc.html | 10 | report page | n/a | Document, not a screen |
| S24–S26 | ConsumerHome_v1 · Biodata_v1 · MySessions_v1 | — | — | — | SUPERSEDED, do not implement |
| D1–D3 | my-qrsetu/Direction · Integration · BiodataAudit | 1, 2R, 37/38 | documents | — | Read as decisions; the audit's 15 items are closed in the design |
| M1 | consumer-data.js | 40 | CONSUMER_TABS, consumerBarTabs, CONSUMER_MORE_TOOLS, consumerMoreGroups, HOME_SECTIONS + homeSections(present,{marketplace}), PERSONAL_KINDS (5), CONSUMER_QR_MAKERS (15), FREQUENT_MAKERS (4), ACCOUNT_GROUPS, NOTIF_CHANNELS/TOPICS, QUIET_HOURS, APP_LANGUAGES, PRIVACY_ITEMS, HELP_TOPICS, APP_META, NOTIFICATION_KINDS (8), notificationsFor, actionItems, creationMeta, personalStories, LIFE_STATES | @qrsetu/domain/consumer/* ports the round-16 subset (CONSUMER_HOME_SECTIONS, CONSUMER_QR_TOOLS, notifications, preferences) | Port to @qrsetu/domain/consumer/* (F1, F2) |
| M2 | biodata-core.js | 39 | 66 constants, 109 functions; FIELDS = 60 (about 11 · work 6 · family 10 · community 4 · kundli 9 · expect 12 · talk 4 · contact 4; basic 24 · released 32 · private 4; required 12); TIERS, PLAN {subjectCap 1, personLinkCap 6}, RELEASE_DAYS 90, EXPIRY_WARN_DAYS 10, REMOVAL_HOURS 72, MAX_PHOTOS 4, MAX_PEOPLE 10, MAX_CUSTOM 8, MAX_LEAD 3, MAX_OPENING 3, AGE_FLOOR 18, AGE_CEILING 75, THEMES (12), ORNAMENTS (3), FAMILY_LAYOUTS (4), LANGUAGES (mr hi en), LANG_METRICS, ENUM_FIELDS (12), NON_TRANSLATIONS (8), EMBLEMS (28, 13 seeded), LIFT_CONTRACT | Nothing | Port to @qrsetu/domain/biodata/* (F4) |
| M3 | biodata-family.js | 39 | tree · vine · cards · list, one node model, caller's tier filter | Nothing | Port as one shared component (RN + DOM) (F4, F6, F7) |
| M4 | qr-registry.js | 35 | TYPES order: order · item · order_receipt · biodata_share · biodata · identity · setu_card; RESERVED; parse/resolve/isForAudience; setOwnerKindLookup; STORES → web app + APK; deepLink | Nothing | Port to @qrsetu/domain/scan/registry.ts (F9) |
| M5 | qr-verify.js | 35 | VERDICT verified · unknown · caution · danger; parsePayload (upi · url · text · empty); lookalikeOf; verify(raw, ctx) with business · codeStatus · reports · vpaOwner lookups | Nothing | Port to domain; lookups from an RPC (F9) |
| M6 | meetings-mine.js + mobile-console/meetings-core.js + platform/meeting-providers.js | 35 | rule + exceptions; per-occurrence invite state; VIEW_MODES; invitations/upcoming/nextUp; NEEDS_ACCOUNT; PROVIDERS; joinRoute; shareChannels; NOTIFY_KINDS; fromAuthResponse | Nothing | Port to @qrsetu/domain/meetings/* (F12) |
| M7 | progress-core.js | 40 | FIELDS weight/waist/hip, HABIT_SUGGESTIONS, GRANT_SCOPES, streak, sharedView, deleteEverything | Nothing | LATER (F15) |
| M8 | chat-core.js, chat-media.js, chat-media-engine.js, voice-notes.js | 23, 24 | shared with the merchant half | @qrsetu/domain/chat/* ports organisation, messages, presence, search | Text chat LAUNCH; media LATER (F16) |
| M9 | brand-qr.js, emblem-art.js, image-slot.js, qr-toast.js, confirm-delete.js | — | brand QR encoder with the Akaya plate; drawn Om; toast and destructive confirm | @/ui has Toast, ConfirmSheet; QR is QRDisplay-style with qrcode | Brand QR = one @/ui primitive (design-first, ADR-0015) (F8) |
⚠ Two design-internal contradictions, carried as decisions rather than resolved silently. (1) consumer.prompt.md and every inline registration sheet (ItemView, VendorView, Chats) still say email or Google, then a 6-digit code by email; the product and the onboarding registry (rounds 11–14) are WhatsApp OTP first, Google a permanent fallback, email never (decision D8). The consumer sign-in spine already built governs; the marketplace sheets are OFF at launch, and the Chats registration gate must reuse the real spine (QRS-912). (2) meeting-providers.js marks Zoom live while meetings-core.js says the connected path is designed and not built; see C1 and QRS-1014.
2 · Architecture the plan builds on, and what it extends
| Layer | FIT (reuse as is) | EXT (extend, justified) | NEW (does not exist) | FUTURE (design now, build later) |
|---|---|---|---|---|
| Identity and session | auth.users + public.users (display_name · avatar_media_id · locale · timezone · primary_context · status), handle_new_user, slugs registry with user_id, get_my_context, get_my_slug, claim_slug, WhatsApp OTP spine, PIN gate + AppLockGate | manage-account: soft delete (status='deleted', QRS-909), set_avatar, mark_notifications_read, export_data, report_code; primary_context set at sign-up (QRS-917); E.164 + on auth.users.phone (QRS-936) | — | global sign-out and session revocation (I2 of the auth review) |
| Data access | packages/data seam idiom; RPC SECURITY DEFINER + REVOKE ALL FROM PUBLIC; EF writes with required idempotencyKey; TanStack Query; Zod in packages/schemas | real impls behind chat, consumerActivity, consumerPrefs (server-side prefs), account | seams biodata, meetings, scan, qrMakers (client-only) | progress, introCard, photoAlbum |
| Media | media table, presigned R2 upload via manage-media, _shared/r2.ts, expo-image-manipulator, expo-image-picker | media.owner_user_id + three-way XOR and the second constraint media_purpose_matches_scope widened (R28); purposes avatar for a user, biodata_photo, biodata_emblem; manage-media accepts an owner scope; private bucket wiring | derivative pair (C4); tombstoning on delete (R8, QRS-1012) | media.sweep drain (needs a scheduler, X4) |
| Messaging (WhatsApp) | _shared/whatsapp.ts + _shared/communication.ts + communication_messages ledger; send-auth-otp proves the path | two utility templates (biodata subject notice; meeting invite/change), Meta review (QRS-1013) | — | campaigns dispatcher (ADR-0029) |
| Chat | tables conversations · messages · conversation_states · labels · reports, read RPCs, @qrsetu/domain/chat/*, realtime policy | — | manage-chat EF (send text · mark read/unread · set state · labels · block · report) — the write path the RLS comment names and nothing implements | chat media (chat_* purposes exist, no presign path), voice, forward, star |
| Entitlements | feature_grants with user_id scope, limit_value/period, useFeature three axes | rows for consumer.biodata (subject cap 1, person-link cap 6); a num_nonnulls XOR CHECK on the seven scope columns (R27, core entity → architecture proposal page first) | — | plan rows for locked personal kinds (Invitation, Birthday) |
| Notifications | derived in-app list (consumerNotifications), expo-notifications local scheduling | kinds biodataRequest · biodataRemoval · biodataExpiry · meetingInvited · meetingChanged · meetingCancelled; per-account read state table (D-c) | localNotifications.ts port (tier-agnostic) | push (no token table, no sender, entitlement stripped) |
| Rate limiting | EF-side counts over idempotency_keys (per user); OTP budgets | — | public.rate_limits bucket + _shared/rateLimit.ts for anonymous reads and access requests (QRS-921) | WAF / Turnstile in front of apps/web |
| Web | React Router v8 SSR, headers export, Cache-Tag purge, Worker runtime, THEMED_ROOT_SEGMENTS already reserves consumer | RESERVED first segments (biodata, intro, photos, o, order, item, c, app, help, legal, download) kept in one domain constant and asserted equal to is_slug_reserved's seed (R30) | apps/web/src/tiers/consumer/ (biodata public page, D5 greeting) | intro card page, album page, report-abuse queue (Apple 1.2) |
| Gates | check:design-parity P1–P7 (reachableFrom), check:screens, check:sql, check:rpc, pgTAP, Deno, jest-expo, device suite | re-transcribe the ledger from round 40 (R24); a contract per new screen | domain test asserting registry RESERVED == SQL reserved seed | journey walk over the consumer routes (QRS-630) |
Rules binding every phase: anonymous-first (no session gate on browse; AppLockGate is a no-op without a session) · EF is the write-enforcement layer, RLS is defence in depth · every RPC name is a *_RPC constant · nothing in apps/mobile branches on plan, persona or industry · no privacy reassurance at the point of use (QRS-549) · never a store CTA in the app (ADR-0002) · Marathi first, the family's content never translated · the design's pure modules are ported once into @qrsetu/domain and consumed by the app and the web alike (the design's own "cannot drift" rule).
3 · Feature by feature
Each card answers the same questions: design scope → what fits today → built and needs wiring → client development → backend that exists → backend to modify → backend missing → schema/RLS/storage → state, navigation, data flow → auth/authz → supporting infra → browser vs native → states → security and privacy → tests → dependencies and sequence.
F1 · Chrome and navigation (tabs, centre button, More sheet, header) — LAUNCH, NEW
- Design.
CONSUMER_TABS= home · chats (badge) · saved · meetings (page) · orders (header:true) · account (header:true);consumerBarTabs()splits 2 + centre + 2; the centre button opens the More sheet ("Make and do") with three groups fromconsumerMoreGroups(): Make something (personal kinds except contact) · QR tools (15 makers) · Tools (Scan a code · My order code · Recently scanned · Share my contact). Header: bell with unread count, reminders clock, avatar → Account (never a tab). Same bar on Home, Chats, Account, MyQRSetu, BiodataView, feeds. - Fits today. Expo Router
Tabs(used once, merchant),PressableScalecentre action pattern,ActionLaunchersheet pattern,@/uiSheet,sessionStore,AppLockGatehost. - Built, wire only. Nothing: the tier is a flat
Stackwith 7 push sites, all from Home. - Client.
consumer/_layout.tsx→AppLockGate→Tabs(Home · Chats · Saved · Meetings) with the centre slot rendered as a non-navigating action;ConsumerMoreSheetfed by the domain port ofconsumerMoreGroups();ConsumerHeader(bell, reminders, avatar);consumerBarTabs()port is the only place the split is decided (design round 39 rule);?tab=and?sheet=deep links. Marketplace OFF: Saved renders the design's honest empty state, Orders stays header-only and reachable from Account (design), the QR-tools group shows only makers that have a screen (see F13). - Backend. None for the chrome. Bell count reads F10; reminders read F12.
- State / nav. Tab state is route state; sheet state is local; unread badge =
totalChatUnreadfrom the chat seam. Tab screens stay mounted (the driver caveat in CLAUDE.md applies to tests). - Auth. Bar and sheet render for anonymous users; items that need a session (My order code, contact card, Make something) open the sign-in spine, not a wall.
- Browser vs native. Identical (RNW). Web
/app/consumer/...mount per ADR-0028. - States. Badge 0 / n / 9+; sheet loading none (data is static).
- Tests. jest: bar renders
consumerBarTabs()output, centre button opens the sheet, header avatar routes to Account, anonymous render; contractconsumer-chrome.jsonwithreachableFrom. - Depends on. Nothing. Unblocks every other feature's reachability. Phase P3.
F2 · ConsumerHome, the identity hub (round 40) — LAUNCH, rebuild (EXT of the existing screen)
- Design. Section stack from
homeSections(present, {marketplace:false}): identity (pinned; greeting, permanent address with brand QR and copy, pulse chip, Share my card / Confirm my number, My QR setu; guest variant claims the address; registered-nothing-published shows the three-step "make a marriage profile") · stories (rings per activated experience, state frompersonalStories(); dashed Make new) · focus "Waiting on you" (snap carousel fromownerPrompts(), count chip, dots) · today (sessions accepted, check-in due, unread announcement) · quickMake (Wi-Fi · UPI · Link · Photos) · progress (journey()ring, four milestones, next step) · yourCards (creations withcreationMeta(): access facts for a biodata, never scan counts) · create (lead card + tiles fromPERSONAL_KINDS) · weekRecap. The sixmarket:truesections are ABSENT. 9homeStates; sheetsscan · code · more · create · recent · contact · reminders(thearea · filters · reg · catssheets are marketplace-only). Bell, avatar. - Fits today.
IdentityCard.tsx+ClaimAddressSheet.tsx(claim flow real:get_my_slug,claim_slug,checkSlug),sessionStore, the D1 identical-availability copy,useFeature. - Built, wire only. Identity card and claim sheet (keep; extend with pulse chip and My QR setu).
- Client. Replace the round-16 stack (search, categories, near you, QR row) with the round-40 stack; every section self-hiding;
presentcomputed from one overview query; carousels with snap and dots; the three-step first run; the reminders sheet reads meetings; Recently scanned and Share my contact sheets. Domain port:HOME_SECTIONS,homeSections,personalStories,creationMeta,creationsFacts,LIFE_STATES. - Backend exists.
get_my_slug,get_my_context,claim_slug. - Backend new.
get_my_biodata_overview()(record + access rollup + journey inputs; F4),get_my_consumer_activity()(F10),get_my_meetings()(F12). Today and weekRecap read progress check-ins and coach announcements that have no schema; both stay absent by the self-hiding rule and are contract rowsblockedwith the reason (R15). - Schema. None of its own.
- State / data. One
useConsumerHome()hook composing three queries with distinctstaleTime(identity 5 min, overview 30 s, meetings 60 s); optimistic nothing; offline renders cached data with the design's offline treatment; error is an error, never an empty state. - Auth. Anonymous sees identity in guest mode (claim CTA) and nothing else that needs an account.
- Infra. Bell count from F10; analytics
home_viewed(id only). - Browser vs native. Copy address:
expo-clipboard(absent, D-i) or the web clipboard API; brand QR viareact-native-svgon both. - States. All 9
homeStates + loading + error; every section's absent state; carousel with 1 / n cards; guest vs registered identity. - Security. Address hardcodes
qrsetu.comtoday (QRS-992): read the origin from config per env. - Tests. Contract
consumer-home.jsonre-enumerated from round 40 (currently 21/39), every section rowreachableFrom; jest perhomeState; device cases for the copy control and carousels. - Depends on. F1, F3/F4 overview RPC (against the stub first). Phase P3, finished in P5.
F3 · My QR setu, the identity home — LAUNCH, NEW
- Design. 7 states. Address card (initials, name, mono address, copy, brand QR 60 px, address note, Share my card sheet with a 150 px code and copy, gear → Account). Intro card, your bio link row (see F14). Activated: the biodata card with status chip + life chip, three access facts (waiting · released · fields never shared, or concluded facts), four card actions (People and access · Open the profile · See it as a family sees it · Open the page in a browser), concluded variant (See the concluded page · Reopen the search) with the reopen note. Nothing activated: the three start steps and Make a marriage profile. Available: Invitation · Birthday card, visibly locked, "On a plan", "Managed on qrsetu.com" (fifth rule, never a purchase CTA). Your identity rows → Account views (address · language · what reaches you · privacy). Footer: nothing here is indexed. Bar + centre inherited.
- Fits today. Identity data (
get_my_slug,get_my_context),useFeaturefor the locked kinds, Account routes,Sheet. - Client. New feature
tiers/consumer/features/identitywithMyQrSetuScreen;ShareCardSheet;LockedCapabilityRowfrom@/ui(fifth rule presentation). - Backend exists.
get_my_slug,get_my_context,feature_grants(availability/entitlement rows forconsumer.invite,consumer.birthday= not shipped). - Backend new.
get_my_biodata_overview()(shared with F2). - Auth. Session required (a destination, reached from three places; guest sees the claim path).
- States. 7 designed + guest.
- Security. Contact card by QR is name + number only: the address page (D5 greeting) must render byte-identical for every state and never reveal the number to an anonymous visitor without the owner's action; the "scan gives your name and number" promise needs the greeting page to render the number only when the owner has enabled it (decision D-t).
- Tests. Contract
my-qr-setu.json(7 states, 4 card actions, locked rows); jest. - Depends on. F1, F4 overview. Phase P5.
F4 · Marriage biodata, the editor (content view) — LAUNCH, NEW
- Design. Hub: live-editing warning, progress arc + headline from
journey(), four thresholds, one weighted recommendation, a card per section (9 groups; status done / started / N needed / not started; stake required / recommended / optional; the tier that mostly reads it), see-it-as-they-see-it card, content-language chip, tier control in the section head, URL + copy + branded QR, the 12-theme How it looks strip, the publish declaration, liveness footer (live dot, last change, hand-off to Share; Publish only for first publication). Section screen: head, why a family cares, who sees it, one tip, fields, custom fields, the photograph block (About), the family-layout picker (Family), All sections / Next. Field sheet built from the field declaration: hint, tier, what an empty field means,choice · suggest · multi · toggle · list · date (year→month→day,AGE_FLOOR 18,AGE_CEILING 75) · derived (age), per-field visibility switch (required fields have none), clear, save. Custom fields: 6 types, 5 presentations, max 8, label ≤ 28, duplicate and standard-field conflict refused, up to 3 lead, live preview, 8 suggestions, reorder. People sheet: structured people (relation · name · detail · seniority · order), max 10,validatePerson. Photos: up to 4 slots, cover = first shown, show/hide, move, remove; 4:5 geometry; honest claim (PHOTO_CLAIM, no download control, no screenshot claim). Opening composer: collection (28 emblems, 13 seeded, tags filter never gate) · words (10 presets) · own picture (upload rules) · off; kuldaivat offered first; ≤ 3 emblems, ordered, each with optional words. Themes: 12 records (wash · pair · accent · ornament), no accent ink. Languages: UI in mr/hi/en; content never translated;formatValueonly forENUM_FIELDS; Devanagari metrics as tokens. States: 10 content × 2 subject; sheets 13. - Fits today.
media+manage-mediapresigned upload (after EXT),expo-image-manipulator(derivative),feature_grants,Sheet,ConfirmSheet,Toast,TextField,ChoiceGrid,PillSelect,Calendar. - Client. Feature
tiers/consumer/features/biodatawithBiodataHubScreen,BiodataSectionScreen,FieldSheet,CustomFieldSheet,PeopleSheet,PhotoSheet,OpeningComposerSheet,ThemeStrip,TierSheet,ShareSheet,ConcludeSheet,ReopenSheet;consumerDraftStore(persisted, cleared on save). Domain port@qrsetu/domain/biodata/*:FIELDS,GROUPS,TIERS,INPUTS,ENUM_FIELDS,VALUE_MAPS,PLACES,LANG_METRICS,journey,sectionState/Status,nextAfter,completeness,validatePerson,validateCustom,conflictFor,publishedCustom,leadCustom,formatValue,isShippedOption,dobRange/ageFrom,photoSet/photoOrder/visiblePhotos,openingOf/openingEmblems/kuldaivatOffer/moveEmblem,look/ornament,words(owner vs subject copy),shownFields/isShown/canHide. Schemas:packages/schemas/biodata.tsgenerated fromFIELDS. - Backend exists. Nothing for biodata.
- Backend modify.
manage-media: owner-scoped upload (owner_user_id, private bucket, purposesbiodata_photo,biodata_emblem), keyu/{userId}/{purpose}/{uuid};feature_grantsrows for the caps. - Backend new.
manage-biodataEF:create · update (version) · set_photos · set_opening · set_custom · set_theme · set_family_layout · publish · unpublish · conclude · reopen · retire;get_my_biodata(id);get_my_biodata_overview(); reference minting (D6 Feistel permutation overmarriage_biodata_ref_seq, key in an EF secret);biodata_templates(D7 sibling) + adefault/1manifest inpackages/schemas(R16). - Schema / RLS. §4.1:
biodata_subjects,marriage_biodatas(typed gate columns + jsonbvalues · field_tiers · hidden · people · photos · opening · custom_schema · custom_values,version,reference,template_key/version,family_layout,theme_id), one active per subject (partial unique index); owner RLSowner_user_id = auth.uid();revoke all;COMMENT ONeverywhere;citextslug FK toslugs. - Data flow.
useBiodata(id)(30 s),useMutationper action with the readversion; 409 → reload or overwrite; drafts persisted locally; every save recomputesjourney()client-side from the same domain module the server validates with (R4: the EF rejects unknown keys and anyfield_tiersentry that loosens aprivatefield). - Auth. Owner only; co-manager read-only is a later grant model (the design state exists; ships as
blockeduntil a co-manager relationship exists in schema, decision D-u). - Infra. D3 subject notice over WhatsApp on publish (utility template, QRS-1013) with a view link and a removal link; publish returns
notice: sent | failed(R17); analyticsbiodata_published. - Browser vs native. Editor is native/RNW only; photos via
expo-image-picker(permission refused = designed state); image manipulation identical on both. - States. 10 content states,
Could not save(what survived),Loadingskeleton,Read only; field validation on required; every sheet's empty state; caps reached. - Security / privacy. Private fields unreleasable by construction (
tierSees); the reference is never sequential; photographs private-bucket only; derivative watermark is the primary control (QRS-923); no PII in analytics; the family's words never translated or normalised; no picker for religion or caste (design's refusal, kept). - Tests.
node --testfor every port (both directions, including the Feistel bijection); pgTAP RLS (owner · other consumer · anon · merchant) and projection; Deno per action incl. version conflict and idempotent replay; jest per content state; contractbiodata.json; device cases (picker, keyboard, Devanagari input). - Depends on. P1 schema, media EXT, feature rows. Phase P4 (backend) → P5 (client).
F5 · Biodata, people and access — LAUNCH, NEW
- Design. 8 access states. The waiting request (what they asked, number confirmed by one code, how they used the link, what they see today, the honest "QR setu checked nothing else" line; Release / Decline), the forwardable link as a count + city with its limit stated, per-person shares with tag, expiry chip, note; Release more opens the release sheet with expiry choice; withdraw (destructive confirm); extend; the subject removal card the owner cannot refuse (
REMOVAL_HOURS 72); conclude sheet → in-app celebration; reopen sheet restating both promises (zero releases, nobody returns). Plan line: free plan 6 person links, forwardable link not counted. - Backend new.
biodata_shares(token hash only),biodata_access_requests, EF actionsmint_share · release · withdraw · extend · answer_request (release | decline) · request_removal (signed token from the D3 message, no auth);get_my_biodata_shares(id); access rollup in the overview RPC. - Rules in schema/EF.
expires_at = released_at + 90 days; lapse at read time; reopening after concluded withdraws every person share; the person-link cap is afeature_grantsrow; a withdrawn or expired token resolves to the closed page, never 404 (R2). - Notifications. kinds
biodataRequest · biodataRemoval · biodataExpiry(in app) and the Today strip rows (actionItems()port); WhatsApp to the requester on release is a later channel (no template yet; state it). - States. 8 designed; celebration; removal countdown copy computed from
removal_requested_at. - Security. Removal honoured by stopping service immediately (C3), erasure by runbook (X4); request phone numbers are E.164-validated (R7) and never shown beyond the owner.
- Tests. pgTAP: share hash lookup, expiry at read time, one active per subject; Deno: every action; contract rows for all 8 states.
- Depends on. F4. Phase P4 → P5.
F6 · Biodata, in-app reader (BiodataView) — LAUNCH, NEW
- Design. Roles
released · basic · owner; states Ready · Loading · Network error · Link withdrawn · Search concluded. Recipient: standing block (who shared, tier, lapse), photo carousel- lightbox (cover only on a forwardable link, reason stated), identity line, the family's own paragraph, work, expectations, community, family drawn in the creator's layout, kundli (released), place map (area, never a pin), who to talk to (call + message inside QR setu), footnotes, same page in a browser (QR + copy), growth CTA. Owner preview: tier switch (Anyone / Released) with the design's sentences, Edit and People in the band. Menus: recipient Keep this profile / Remove from my profiles, Report this profile (family not told who); owner Edit / People and access. Ask to see more is a signed request with three asks (family · community and kundli · the rest of the photographs) + note; pending state. Share sheet: code · copy · more apps; forwards the basic address only.
- Backend. Reads
get_public_biodata(p_slug, p_token)through thebiodata-readEF wrapper (rate limit → RPC → record the open via service role, R1);access_requestwithrequester_user_idset;keepneeds a per-user list →biodata_kept (user_id, biodata_id, share_id), new, small; report needs a moderation destination →biodata_reports(append-only, admin-read later; QRS-803), new. - Message inside QR setu → a conversation with the family (a user, not a workspace): today
conversations.workspace_idis NOT NULL and the consumer-to-consumer case has no model. This is a real gap (R18b). ⚠ SETTLED 2026-09-05 by ADR-0032, which locks the chat identity model. A person is reachable in-app by capability, and a conversation is between PRINCIPALS rather than between a workspace and a consumer, so person-to-person messaging is a modelled destination rather than a permanent redirect. R1 still routes the talk action to WhatsApp on the number the family released — the schema change is sequenced beforemanage-chatand the in-app path follows it, not this screen. - States. 5 designed × 3 roles; galleryLocked for basic readers.
- Tests. Contract
biodata-view.json; jest per role/state; the shared family component tested once in domain. - Depends on. F4, F5, F16 (D-v). Phase P5.
F7 · Biodata, the public page and the address greeting (apps/web) — LAUNCH, NEW
- Design.
/<slug>/biodata(basic) and/<slug>/biodata/<share>(per person), noindex; 9 scenarios incl. lapsed / withdrawn / concluded (never a 404), removal confirmation (subject view), loading; the WhatsApp link preview (first name, neutral line, wordmark, no photograph at any tier); language pills mr/hi/en (UI only); opening block; photo hero carousel with lightbox and attribution strip; family layout param from the record; theme via three custom properties; access-request form (name · relationship · phone · asks); who to talk to (call, WhatsApp help line); share pill + end-of-reading share card; app offer (web app or signed APK, no store); growth CTA;In discussionstatus note. D5 greeting at/<slug>for a person: byte-identical for claimed / unclaimed / private. - Fits today. SSR route family,
headersexport (noindex,no-store,Cache-Tag: biodata-{id}), the card's OG pattern,setu-card-redirectfor merchants. - Client (web).
apps/web/src/tiers/consumer/features/biodata/routes:slug/biodata,:slug/biodata/:share, plus:slug→resolve_slug_owner_kind(merchant → 301 to the card, person → greeting); the same domain module and the shared family component;report abusecontrol (Apple 1.2, never cut). - Backend.
get_public_biodata(tier-projected, private never),biodata-readEF wrapper (rate limit per token and per source hash, fail closed),access_request(anon, rate-limited),resolve_slug_owner_kind(anon-grantable, answers onlyworkspace | user | null, identical for reserved / released / never claimed, R9, R23), presigned derivative URLs minted inside the wrapper for seconds. - States. All 9 + preview + greeting states.
- Security. Enumeration-safe greeting; bearer share tokens compared by hash and state-checked on every read; rate limits;
no-store; no photograph in any OG; report path. - Browser only. This surface is DOM only, by ADR-0011/0019.
- Tests.
run-webdriver asserts headers on the built server; vitest per scenario from a fixture; axe; pgTAP tier projection never leaksprivate; the oracle byte-identity test. - Depends on. F4/F5 backend, P0 production 500 fix (X1). Phase P7 (routes) after P4.
F8 · Sharing, QR rendering and deep links (cross-cutting) — LAUNCH, EXT + NEW
- Design. One brand QR encoder (
brand-qr.js: matrix + Akaya "QR" plate, light plate in both themes) used by the identity card, My QR setu, biodata hub and reader, order code, meetings share, makers; share sheets always: code first · WhatsApp · copy · the OS share sheet; makers add Download image (PNG with caption).qr-registry.jsdeepLink()andSTORES(web app + APK, no store listing). Sharing is by pointer, never by copy; no PDF or image export of a biodata (D-e / QRS-1010). - Fits today.
qrcode(matrix),react-native-svg,expo-web-browser,setuCardShare.{native,web}pattern,QRDisplaydesign-system component. - Client. One
@/uiBrandQrprimitive (systemic surface → design pull + drift-ledger row, ADR-0015) consumed by every consumer surface;ConsumerShareSheetwith the fixed channel order; auseShareByPointer(target)hook wrappingShare/navigator.share; PNG export on native needs a raster (no canvas):react-native-view-shot(absent, size callout) or server rendering. ⚠ Server rendering is rejected for the Wi-Fi and UPI makers: their payload (a router password, a UPI id with amount) would leave the device (R19). On-device capture only. - Deep links. Android app links need the release signing fingerprint (after QRS-981); iOS associated domains need the Apple team (D-U-N-S pending);
apps/webservesassetlinks.jsonand AASA frompublic/;intentFiltersinapp.json; until then the web page's Open in app usesqrsetu://and the design's honest APK/web copy. - Backend. None. Infra. Analytics
share_minted,card_shared(ids only). - Browser vs native. Web:
navigator.sharewith file support detection, clipboard API; native: OS share sheet,expo-clipboard(D-i). - Tests. Domain:
deepLink,shareTarget; jest: sheet order; device: share on Android and iOS. - Depends on.
BrandQrdesign pull. Phase P3 (primitive) → P7 (links).
F9 · Scan and verify + the QR registry — LAUNCH, NEW
- Design. Camera viewfinder (dark surface in both themes), camera-denied full screen with the way through; every payload → registry first (
order · item · order_receipt · biodata_share · biodata · identity · setu_card); a QR setu code opens after a half-second interstitial naming what was scanned (no verdict sheet), except that asetu_card/itemstill passes the verdict engine (cancelled sticker, unknown slug) and a slug the app cannot render refuses honestly; wrong audience (an order token) is refused plainly; non-QR-setu payloads → the four-verdict sheet with signals, payee block for UPI, business block (name · category · area · since; no scan count, QRS-576), scope note, actions (Open · Continue in my UPI app · Report this code · Scan another); report sheet with five reasons → moderation; scan history kept (last 25). Recently scanned sheet on Home reads it. - Fits today.
src/ui/scanner/shell (built for this, zero importers;expo-camerais its only importer, parity R9),getUserMediafallback on web. - Client. Feature
scan-verifyon the scanner shell; domain portsscan/registry.ts(types as data,parse/resolve/isForAudience,RESERVED) andscan/verify.ts(verify(raw, ctx)with injected lookups);scanHistoryStore(device, capped); the opening interstitial; deep-link handoff to VendorView / ItemView / OrderCode / BiodataPage / MyQRSetu. - Backend exists.
get_public_setu_card(business facts),resolve_slug_status(authenticated). - Backend new.
resolve_slug_owner_kind(p_slug)anon (R23);verify_qr_lookups(p_slug, p_code, p_vpa)returningbusiness · codeStatus · reports · vpaOwneras one read (the design'sctx), withcodeStatusunknown until a minted-code registry exists (QRS-576) andvpaOwnernull until vendor VPAs are stored, both stated in the response;manage-account report_code→ aqr_reportsappend-only table (community reports are the other half of the safety layer; the count feeds thereportedsignal). Rate-limited (§2.6). - Browser vs native. Camera: native
expo-camera; webgetUserMediawith the denied state; the UPI action (upi://intent) is native-only, on web it shows the payee and copy. - States. Scanning · denied · opening · wrong audience · 4 verdicts × signal sets · report form · reported; offline (lookups fail → unknown with the honest scope note, never verified).
- Security. Fail closed: no lookup means no green tick; the registry order (biodata before setu_card) is asserted by test; the owner-kind oracle is identical for all three unknowns.
- Tests. Domain: the 9 demo payloads reproduce the design's verdicts; jest; device (camera).
- Depends on. F1 (permanent Scan entry),
resolve_slug_owner_kind. Phase P6.
F10 · Notifications — LAUNCH, EXT
- Design. Bell → list grouped Today / Earlier, derived by
notificationsFor()from stored activity; 8 kinds (orderReady · orderConfirmed · paymentFailed · message · newItems · biodataRequest · biodataRemoval · biodataExpiry) plus meetings (invited · reminder · changed · cancelled); each links to its object; reading clears; Mark all read; empty state; Notification preferences → Account › notifs. Read state per account on lift (D-c, QRS-1011). - Built, wire only.
NotificationsScreen+@qrsetu/domain/consumer/notifications.ts(5 kinds). - Client. Add kinds; swap
consumerActivityServicestub for the real impl; optimistic mark-read. - Backend new.
get_my_consumer_activity()(biodata requests/removals/lapses, meetings invited / changed / cancelled, unread chats; orders only when the marketplace is on);consumer_notification_reads (user_id, notification_id, read_at);manage-account mark_notifications_read. No notifications table (the list stays derived). - Infra. In-app only at launch; local reminders are F12's; push absent (stated).
- States. default · empty · loading · error · offline (cached list).
- Tests. pgTAP for the reads table; Deno; jest; contract
consumer-notifications.json. - Depends on. F4/F5 kinds, F12 kinds. Phase P6.
F11 · Account (profile and settings) — LAUNCH, EXT
- Design. 8 views. hub: guest banner (Confirm your number) or Edit profile; Saved · Orders · Scans counters;
ACCOUNT_GROUPS(Personal: My QR setu · Personal details · Your areas · Your contact code; Your activity: Saved · Orders · Recently scanned; Preferences: Notifications · Appearance and language · Privacy and data; Support: Help · About); merchant pitch card; Sign out (confirm, toast). details: change photo, full name, mobile (validation), email, Save, Delete my account (confirm). areas: marketplace. notifs: channels (In app locked on · WhatsApp · SMS · Email), topics (orders and payments locked on with why · chats · saved · offers · seasonal), quiet hours. appearance: theme Light · Dark · System, language mr/hi/en, rupee note. privacy: saves and recent scans (Clear) · history (Clear) · Download my data (Request). help: FAQ accordion, message support. about: wordmark, line, version, legal. - Built, wire only.
AccountScreenhub + notifs against the stub;useSignOut(merchant) reuse;themeStore,localeStore. - Client. Remaining views;
consumerPrefsreal impl (server-side prefs onusersor aconsumer_prefsjsonb column so the phone and the web agree, decision D-w); details save; avatar via owner-scoped media; delete flow; counters (Scans from the device history; Saved/Orders absent with the marketplace). - Backend exists.
set_my_display_name,update_email,delete_account(hard),get_my_context. - Backend modify.
delete_account→ soft delete (users.status='deleted', QRS-909) that retires biodatas, revokes shares, tombstones media, releases the slug (trigger exists), and stops purging the undeclaredprofile-picturesbucket;set_avatar;update_phonegoes through the phone-change OTP (a full verification, not a field write);export_data(request recorded, delivery later);set_prefs. - Honesty of channels (D-p). Only In app and WhatsApp have a sender; SMS (QRS-952) and email do not exist. Render SMS and email absent (availability axis), never as toggles that do nothing.
- Auth. Guest sees the hub with the banner; every write needs a session; delete requires the confirm and, per the app lock, a fresh PIN/biometric when set (decision D-x).
- States. Registered · Guest · Loading · Error × 8 views; validation on details; toasts.
- Security. Deletion cascade documented and tested (R8); export request logged; no privacy reassurance copy at the point of use (the design's privacy intro is a statement of what is stored, allowed).
- Tests. Contract
consumer-account.json(8 views × 4 states); jest; Deno for the EF actions; pgTAP for the soft-delete cascade. - Depends on. F1, media EXT,
manage-accountEXT. Phase P6.
F12 · Meetings, participant half (LAUNCH, HONEST-EMPTY) and host half (LATER, C1 / QRS-1014)
- Design. Calendar view: Day · Week · Month · Upcoming as four reads of one timeline, week strip, month grid, the invitation queue above (accept / decline in place, one card per meeting, answer written to the next occurrence), notification banner (New invitation · 15 minutes before), sticky host action; Meeting view: host line (person vs business), when, live / imminent / ended, description, who is coming (Coming · Not coming · Invited; "N not on QR setu got the link"), Join (in app where the provider supports it, fresh link on open), Accept / Decline, Share (send in QR setu · copy · QR · other), reminder toggle (does not decline), host Cancel; New meeting: connect gate (Connect → Authorize with scopes in words → connected / could not connect), four-field form (title with suggestions, day, time, length with plan cap read from the account, repeat none/daily/weekly, invitees, More options); Meeting account: what the integration reported (not reported ≠ unlimited), Disconnect. Scenarios Default · Nothing scheduled · Loading · Error · Offline.
- Client.
meetings.tsxwith?view=; domain portsmeetings/*(occurrencesFor,timeline,inviteState,occurrenceState,monthGrid,weekStrip,invitations,upcoming,nextUp,reminderPlan,joinRoute,shareChannels,NEEDS_ACCOUNT,PROVIDERS,fromAuthResponse);localNotifications.ts(7-day horizon, top-up on open, reopens the meeting, never carries the link); the create and account views render Not connected honestly with the connect gate present but the authorize step disabled until P9 (fifth rule: availability, "not ready", never a dead control). - Backend new (participant). §4.2
meetings,meeting_participants(server reminder prefs, R6),meeting_invite_states,meeting_joins;get_my_meetings(from, days),get_meeting_join(id)(fresh);manage-meeting answer · set_reminder · record_join. Producer: none in scope (C1): the pipeline is proven by pgTAP, Deno and one seeded meeting on Dev. - Backend later (host, P9). Zoom Meeting SDK for web in a QR Setu-hosted page inside a WebView (
react-native-webview, size callout) + Server-to-Server OAuth on QR Setu's own account (H1), provider connections table with encrypted tokens,meeting-provider connect · create_room,meeting-join-tokenEF; the host's own account (H2) after Marketplace review. Decision QRS-1014. - Infra. In-app kinds; WhatsApp invite/change/cancel to numbers (utility template, QRS-1013); reminders local only; a web reader has no local notifications (stated).
- Browser vs native. Calendar identical; local reminders native only; Join in app is the WebView page (native) or the same hosted page in a new tab (web).
- States. 5 scenarios × 4 views × 5 connection states; empty invitation queue; offline shows the link we last saw.
- Security. Join links read fresh; notifications never carry the link; invitee phone numbers E.164, indexed, erasure query documented (R5).
- Tests. Domain (recurrence, DST, occurrence state, reminder budget), pgTAP, Deno, jest; contract
consumer-meetings.json; device: a scheduled local reminder fires. - Depends on. F1 tab, P1 schema. Phase P6; hosting P9.
F13 · Personal QR makers — Wi-Fi and Link LAUNCH; UPI, Photos and the eleven others LATER
- Design.
CONSUMER_QR_MAKERS(15): wifi · upi · link · place · event · photos · doc · social · profile · feedback · poll · survey · assignment · learning · sos, each withctaand threesteps; four have screens (WifiQR,UpiQR,LinkQR,PhotosQR);FREQUENT_MAKERS= wifi · upi · link · photos on Home; the More sheet lists all 15 opening the create sheet on that kind. Maker screens: inputs (Wi-Fi: SSID, password show/hide, WPA/WEP/Open with theWIFI:escaping rule; UPI: VPA validation, name, amount, note →upi://paypayload; Link: URL validation + scheme rule, optional label; Photos: title → album slug + three slots), branded QR preview (not-ready placeholder), Download image, share sheet (WhatsApp preview mock, more, download), footer copy. - Client.
qr-makersfeature withWifiQrScreen,LinkQrScreen(LAUNCH); the create sheet handoff; domainmakers/payloads.ts(Wi-Fi escaping, UPI payload, URL normalisation) tested both ways; the More sheet shows only makers with a screen (availability axis), the other eleven are absent, not "coming soon" tiles. - Backend. None for Wi-Fi and Link as static codes. ⚠ The Link maker's copy promises a re-pointable code ("Change the address later and the printed code keeps working"), which needs a redirect record (
personal_links,/<slug>/l/<id>web route) that does not exist (R20). Decision D-s: launch static with a design copy correction, or build the redirect (LATER). Photos needs an album table, a public album page, owner media and moderation (LATER). UPI is decision D-d / QRS-1009. - Browser vs native. PNG export: on-device raster (F8); web: canvas as designed.
- Security. Wi-Fi password and UPI payload never leave the device and are never logged; makers store nothing server-side at launch.
- States. Not ready · ready · invalid input · download success/failure · share fallback text.
- Tests. Domain payload tests against the design's own strings; jest; device: download on Android.
- Depends on. F8
BrandQr. Phase P6.
F14 · Intro card (personal links page) — LATER, decision D-l
- Design. the intro-card editor artboard (avatar, name, bio, links with presets Setu Card · WhatsApp · Instagram · YouTube · Facebook · any link, reorder, remove; quick actions Message me on QR setu, Let people book time, Pay me by UPI (VPA), Save me as a contact (phone); page QR, preview, share with download) and the intro-card public-page artboard at
/<slug>/intro(links, action sheets: chat with a first-message gate, book request, UPI QR, vCard download, share, growth CTA). - Why later. Needs its own table (
intro_cards, links, actions), a public SSR route, chat and booking request writes (F16, and booking has no consumer model), UPI (D-d), vCard generation, and a name: the design file revives the retired product name (QRS-1007); the product name is Intro card and the route/intro. Nothing in the repo may be called intro card (check:docsgroup #1). - Design it now. Reserve
introinRESERVEDandis_slug_reserved; keep the My QR setu row as the design draws it but rendered as not ready (availability) until built.
F15 · My progress — LATER, decision D-j
- Design. Self-entered record (weight, optional waist/hip), habits and a non-shaming streak, private before/after photos, a revocable grant to one business with scopes shown before confirming, settings (nudge, stop sharing, delete everything), first-use notice with
[legal copy pending review]. Data on the phone until shared; the coach reads on the merchant lead detail while the grant stands. - Why later. No entry point in the round-40 navigation data (not in
CONSUMER_TABS,ACCOUNT_GROUPSor the More sheet; back goes to Account); the coach read is a merchant surface outside this plan; the legal copy is a placeholder; the grant needs aprogress_grantstable and a sync of local entries at grant time. Home's today and weekRecap sections stay absent. - Design it now. Nothing in schema; note the dependency in the tracker.
F16 · Chats (consumer half) — text LAUNCH (NEW write path), media and interactions LATER (D-m)
- Design. List (search over name, category, message text; derived pills All · Unread · Favourites · Archived + user lists; pins capped at 3; category chip from industry; media glyph), thread (day groups, per-message state incl. queued/failed with retry, typing, business-hours status, away system line once per closed period, shared item card with sold state, reply-to, New messages separator, jump to latest, composer that blocking disables), per-conversation actions (Pin · Favourite · Archive · Read/Unread · Mute · Add to list · Open their Setu Card · Report · Block), registration gate holding the draft; round 23 voice, photos, documents (transfer and cache states, permission refused, viewer); round 24 swipe reply, message info, long-press actions (Reply · Copy · Forward ≤ 5 · Star · Save · Info · Delete for me).
- Built, wire only.
ConsumerChatsScreenlist;@qrsetu/domain/chat/*; read RPCsget_my_conversations,get_conversation_messages; RLS, block trigger, reports table, states and labels tables exist. - Backend new (LAUNCH).
manage-chatEF:send (text | item, client_key idempotency) · mark_read · mark_unread · set_state (pin cap 3 enforced server-side · favourite · archive · mute) · create_label · delete_label · set_label_member · set_blocked · report;conversations.updated_attouch trigger andconversation_states.updated_attrigger (measured missing); the away reply emitted as asystemmessage by the EF from business hours (once per closed period). - Client (LAUNCH). Thread screen; the registration gate reusing the WhatsApp OTP spine (the design's email steps are stale, QRS-912); optimistic send with
client_key; realtime subscription through the existingrealtime.messagespolicy. - LATER. chat media presign for
chat_*purposes (a consumer has no upload path today), voice notes engine, viewer, forward/star/info (message_states exists). - States. 4 list × 8 thread states; media states later.
- Security. Block is bidirectional and enforced by trigger; report goes to
conversation_reports; no last-seen anywhere. - Tests. Deno per action incl. the pin cap and block; pgTAP already covers grants and blocks; jest; contract
consumer-chats.json. - Depends on. F1. Phase P4b (EF) → P6 (thread).
F17 · Order code and the marketplace surfaces — OFF at launch
OrderCode(buyer / relative, grants, re-minted token) stays gated (QRS-708): its token must be server-minted (HMAC in an EF) and its grants need a table; both arrive with the marketplace.ItemFeed,VendorFeed,ItemView,VendorView,Featured, Saved tab content, Orders tab content: absent by capability. Their routes remain for the registry's deep links (a scanned Setu Card still opens VendorView, as the design requires), and their inline registration sheets are not touched.- Rows in the ledger stay
built; the parity contract marks marketplace rowsblocked: D-a.
F18 · Auth, session and the app lock (dependencies, mostly FIT)
- Exists: WhatsApp OTP spine, PIN gate with
entry=openreachable viaAppLockGate(P7 route-verified), 30-second foreground grace, clear-PIN-on-sign-out,resolveEntryRouteconsumer branch. - Modify before biodata:
primary_context='individual'must reach the server at sign-up (QRS-917,handle_new_userelse-businessfallback QRS-935);auth.users.phone+prefix (QRS-936); account created on OTP send, not verify (QRS-998) — a consumer who never verifies leaves a phantom row that could claim nothing but must not count; global sign-out (I2). - Security note: the PIN is device-local (no server state, measured); a lost unlocked phone has no remote remedy until session revocation exists (stated).
4 · Data model and backend catalogue
4.1 Tables
| Table | Status | Notes |
|---|---|---|
users | FIT / EXT | status='deleted' written by soft delete; avatar_media_id set by set_avatar; optional prefs jsonb (D-w) |
slugs | FIT | user_id owner; resolve_slug_owner_kind reads it |
media | EXT | owner_user_id; XOR num_nonnulls(workspace_id, conversation_id, owner_user_id) = 1; media_purpose_matches_scope widened for avatar(user), biodata_photo, biodata_emblem (R28); derivative_key; tombstone on delete |
feature_grants | EXT | rows consumer.biodata (subject cap 1 · person links 6); XOR CHECK over the 7 scope columns (R27) via the architecture-proposal protocol |
biodata_subjects | NEW | owner, relation, owner_is_subject, first_name, subject_phone (E.164), content_language |
marriage_biodatas | NEW | typed gates + jsonb bags (§F4), version, reference (D6), template_key/version (D7), family_layout, theme_id, status, life, timestamps incl. removal_requested_at; one active per subject |
biodata_shares | NEW | `kind basic |
biodata_access_requests | NEW | share_id, requester_user_id, name, relationship, phone, asked, state |
biodata_kept | NEW | recipient's kept profiles (F6) |
biodata_reports, qr_reports | NEW | append-only; admin read later (QRS-803) |
biodata_templates + marriage_biodata_ref_seq | NEW | D7 sibling of setu_card_templates; D6 sequence |
consumer_notification_reads | NEW | per-account read state (D-c) |
meetings, meeting_participants, meeting_invite_states, meeting_joins | NEW | §F12; rules + deltas, never expanded rows |
rate_limits | NEW | fixed-window bucket, service role only |
conversations, conversation_states | EXT | missing updated_at triggers |
| LATER | meeting_provider_connections, personal_links, intro_cards, photo_albums, progress_*, order_grants | not in the release |
Every table: RLS on, revoke all from public, anon, authenticated, owner policies by relationship never by role, COMMENT ON for purpose and rules, expand-contract, citext via extensions..
4.2 RPCs (reads)
| RPC | Grant | Status |
|---|---|---|
get_my_context, get_my_slug, get_my_features, get_my_conversations, get_conversation_messages, get_public_setu_card, 5 consumer feed RPCs | as measured | FIT |
resolve_slug_owner_kind(text) → `workspace | user | null` |
get_my_biodata_overview(), get_my_biodata(uuid), get_my_biodata_shares(uuid) | authenticated | NEW |
get_public_biodata(text, text) | called by the biodata-read EF only (service role) | NEW (R1) |
get_my_consumer_activity(), get_my_notification_reads() | authenticated | NEW (the check:rpc forward-declared allowance is retired by this) |
get_my_meetings(date, int), get_meeting_join(uuid) | authenticated | NEW |
verify_qr_lookups(text, text, text) | anon (rate-limited through an EF) | NEW |
4.3 Edge Functions (writes)
| EF | Actions | Status |
|---|---|---|
manage-account | update_email, change_password, claim_slug (FIT) · delete_account → soft (EXT) · set_avatar, update_phone (OTP), set_prefs, mark_notifications_read, export_data, report_code (NEW actions) | EXT |
manage-media | owner scope, private bucket, biodata purposes, derivative pair | EXT |
manage-biodata | create · update · set_photos · set_opening · set_custom · set_theme · set_family_layout · publish · unpublish · conclude · reopen · retire · mint_share · release · withdraw · extend · answer_request · request_removal · access_request (anon) | NEW |
biodata-read | rate limit → get_public_biodata → record open → presign derivative | NEW |
manage-chat | send · mark_read · mark_unread · set_state · create_label · delete_label · set_label_member · set_blocked · report | NEW |
manage-meeting | answer · set_reminder · record_join (participant) · create · update_occurrence · cancel · share (P9) | NEW |
send-auth-otp, whatsapp-webhook | as is; the utility templates are registry rows | FIT |
⚠⚠ CORRECTED 2026-09-10 — THIS SAID IDEMPOTENCY KEYS ARE SCOPED (user_id, action, key) AND THEY ARE NOT, AND A CLIENT WRITTEN AGAINST THE FALSE VERSION 409'd EVERY BIODATA PHOTOGRAPH (QRS-1241). public.idempotency_keys has key as its SOLE PRIMARY KEY and no action column; _shared/idempotency.ts's own header states that "reusing one across operations is detectable instead of silently allowed" and refuses the second call with a 409. So a key is globally unique and every step of a multi-call flow needs its own: issue keeps the caller's key (a duplicate there leaves an orphan pending row), and confirm mints a fresh one, exactly as manage-media's uploadImage already documented. The measured cost of the wrong sentence: photographs uploaded successfully to R2 and never became visible, because the confirm was refused and the row stayed pending. Every action logs a structured event.
4.4 Storage, secrets, templates, config
- R2 private bucket for owner media (bucket name from the row, never env, X1 of the WhatsApp review); CORS file updated for the private bucket's origins.
- EF secrets:
BIODATA_REFERENCE_KEY(D6),BIODATA_REMOVAL_SIGNING_KEY(C3), later Zoom S2S secrets. - Meta utility templates:
qrsetu_biodata_subject_notice,qrsetu_meeting_update(QRS-1013), seeded inwhatsapp_message_templateswithused_by. config.toml: the Send SMS hook is dashboard-only today (measured); record it in the promotion runbook as anauth_settingchange class with a live probe, since no script can see it.
5 · Client architecture
Routes (apps/mobile/src/app/consumer/): _layout.tsx (AppLockGate → Tabs) · index.tsx Home · chats.tsx (+ ?convo=) · saved.tsx (honest empty) · meetings.tsx (?view=) · my-qr-setu.tsx · biodata.tsx (?view=content|people, ?new=1) · biodata-view.tsx (?viewer=) · account.tsx (?view=) · notifications.tsx · scan.tsx · makers/wifi.tsx, makers/link.tsx · existing order-code.tsx, vendor.tsx, items.tsx, item.tsx, vendors.tsx (registry-reachable only).
Features (tiers/consumer/features/): home (EXT), identity (NEW), biodata (NEW), scan-verify (NEW), meetings (NEW), qr-makers (NEW), notifications (EXT), account (EXT), chat (EXT), browse, collection (untouched). Each with screens/ · hooks/ · README.md — the hooks/ layer the tier README promises and no feature has today.
State. Server state in TanStack Query with one queryKey family per seam; client state in Zustand (sessionStore, appLockStore unpersisted, themeStore, localeStore, consumerPrefs mirrored from the server, consumerDraftStore persisted, scanHistoryStore capped). Optimistic updates only for mark_read, answer, chat send. Offline: cached lists with the design's banner; drafts survive a kill; no write dropped silently.
Domain ports (@qrsetu/domain): consumer/* (extend), biodata/*, meetings/*, scan/*, makers/*, all node --test, both directions, consumed by apps/mobile and apps/web.
i18n. New namespaces consumerIdentity, biodata (labels from LABELS.ui, NON_TRANSLATIONS kept verbatim), meetings, scan, makers; Marathi is the reference and human-reviewed (QRS-924); Devanagari metrics as tokens; the family's content is never passed through t().
6 · Security and privacy
| Surface | Control |
|---|---|
| All new tables | RLS by relationship; revoke all; pgTAP asserts a consumer with zero memberships reads nothing merchant-owned and nothing biodata-owned by another user, through the RPC surface (the existing tests grant table privileges first, which is the opposite of production, R36) |
| Share tokens | 128-bit, hash stored, state-checked on every read, closed page never 404 |
| Public biodata read | tier projection, private unreachable, rate-limited, no-store, opens recorded by the EF not the RPC |
| Enumeration | /<slug> greeting byte-identical; owner-kind oracle identical for all unknowns; slug availability stays the D1 vocabulary |
| Media | private bucket, presigned for seconds, derivative only at basic tier, no download control, no screenshot claim |
| Removal | signed single-purpose token, immediate stop of service, 72 h erasure by runbook |
| Deletion | soft delete cascade (biodata retired, shares revoked, media tombstoned, slug released), R2 sweep by runbook until a drain exists |
| Concurrency | version on the biodata; 409 → reload or overwrite |
| Chat | block trigger, report table, no last-seen, EF-only writes |
| Scan | fail closed, no tick without a lookup, report path, scan history device-local |
| Makers | payloads never leave the device |
| PII | phones E.164 and never beyond the owner; Sentry scrubs; analytics ids only |
| Rate limits | anonymous reads fail closed; authenticated owner actions fail open; timeouts allow-once-and-log (R10) |
| Store compliance | no purchase CTA in the app; report abuse on every public page (Apple 1.2) |
7 · Notifications, analytics, webhooks, observability
- In-app: derived list + per-account reads; the Today strip is the proactive surface (no second prompt component).
- WhatsApp: D3 subject notice (publish), meeting invite/change/cancel; both need utility templates through Meta review (critical path, QRS-1013); sent synchronously by the EF through the existing core; ledger row before send; publish reports
noticeoutcome (R17). - Local reminders: meetings only; 7-day horizon; server prefs per participant.
- Push: none (no tokens, no sender, entitlement stripped). SMS and email: none.
- Webhooks:
whatsapp-webhookrecords delivery statuses for the two templates (exists). - Analytics: vocabulary
biodata_published · share_minted · release_granted · access_requested · meeting_answered · scan_resolved · maker_exported(ids and kinds only); the mobile sink is a decision (X7); the web card beacon points at an archived EF (QRS-734) and must not be copied. - Observability: Sentry DSN set for the release build; every EF action logs action + object id + outcome; a daily watchdog query for failed subject notices beside the payments one.
- Scheduler: none exists (X4). Nothing in this plan depends on a cron: expiry is decided at read time, erasure is a runbook, reminders are local.
8 · Browser versus native
| Capability | Android / iOS native | Web (RNW /app) | DOM web (apps/web) |
|---|---|---|---|
| Camera scan | expo-camera | getUserMedia + denied state | n/a |
| UPI intent from a verdict | upi:// | copy payee, no intent | n/a |
| PNG export of a maker code | on-device raster (react-native-view-shot, size callout) | canvas | n/a |
| Clipboard | expo-clipboard (D-i) | clipboard API | clipboard API |
| Share sheet | OS share | navigator.share / fallback copy | navigator.share / WhatsApp link |
| Local reminders | expo-notifications | none (stated) | none |
| App lock / biometrics | expo-secure-store, expo-local-authentication | PIN only | n/a |
| Photo pick + derivative | expo-image-picker, expo-image-manipulator | file input, same manipulator | n/a |
| Public biodata page, greeting, intro (later) | opens in app via registry | same | the only renderer (SSR, noindex) |
| Join a meeting (P9) | WebView on a hosted page | new tab | hosted page |
| Deep links | app links after keystore / Apple team | n/a | assetlinks.json, AASA, Open in app |
Parity: every feature ships on Android, iOS and RNW together; native modules are proven only on the device suite; no exception path (CLAUDE.md).
9 · Testing, regression and validation gates
| Layer | Proves |
|---|---|
node --test (@qrsetu/domain) | every port both directions: homeSections with marketplace off, consumerBarTabs split, journey, tier projection helpers, validatePerson/Custom, Feistel bijection over 10⁹, recurrence and occurrence state, verify reproducing the 9 design payloads, registry order (biodata before setu_card), RESERVED == SQL reserved seed, maker payload strings |
| pgTAP | RLS isolation through the RPC surface; private never projected; one active per subject; share hash lookup and expiry; oracle byte-identity; rate-limit windows; soft-delete cascade; reads table; meetings deltas |
Deno (test:ef) | every action of manage-biodata, manage-chat, manage-meeting, biodata-read, manage-account: success · validation · authz · idempotent replay · version conflict · safe 500 · notice outcome |
| jest-expo | hooks against stubs; each screen per designed state; the bar renders consumerBarTabs(); AppLockGate no-op without session; drafts survive; route tests for src/app/consumer/* (none exist today) |
| Parity contracts | one per new screen, enumerated from the design props, every route row reachableFrom; check:design-parity green; ledger re-transcribed from round 40 so check:screens counts these screens |
apps/web | vitest per public scenario from a fixture; axe; run-web driver asserts noindex, no-store, Cache-Tag on the built server |
| Device suite | new cases per screen appended to guides/device-test-suite-consumer-auth.md, run on Android and iOS: camera, clipboard, share, picker, local reminder, app lock over every new route |
| Static gates | check:sql, check:rpc (allowances retired), check:naming, check:docs (no retired names), check:screens, check:design-parity, check:claims, lint, type-check |
Regression rule: the journey walk (QRS-630) over the consumer routes runs on any change to src/ui/**, packages/** or the consumer _layout.
10 · Distinguished Principal Solution Architect review (independent, adversarial)
R1–R17 from the plan of record are carried unchanged and remain folded in (public read that wrote → EF wrapper; bearer tokens → state check; no concurrency → version; jsonb registry → server validator; fourth phone location → E.164 + erasure query; per-device reminders → server prefs; subject notice to a stranger → confirm step; media outlives account → tombstones + runbook; oracle surface; rate-limit SPOF; client derivative accepted; capability constant accepted; no meetings producer; idempotency scope; hidden Home dependencies; template manifest; notice failure alarm). The following are new, found against the measured repo and the full design surface.
| # | Finding | Severity | Amendment |
|---|---|---|---|
| R18 | There is no chat write path at all. The RLS comment names "the chat Edge Function" as the enforcement layer; no EF touches conversations or messages; every consumer write is a stub. Chats is a tab and the biodata reader promises message inside QR setu. | high | manage-chat EF in P4b (text, states, labels, block, report); media and voice stay P9 (D-m). |
| R18b | Consumer-to-family messaging has no model. conversations.workspace_id is NOT NULL; the design's talk block opens a chat with a person. ⚠ Measured 2026-09-05: business-to-business is unrepresentable for the mirror-image reason (consumer_user_id is NOT NULL), so only one of four communication flows has a model. | high | CLOSED by ADR-0032. A conversation is between PRINCIPALS; the assessment and migration approach are in the proposal, sequenced BEFORE manage-chat so no code is discarded. Every chat table holds zero rows on Dev, which is what makes the change cheap now and expensive later. R1 keeps the WhatsApp route on the released number. |
| R19 | Maker PNG export cannot be server-rendered without leaking a router password or a UPI id. | medium | On-device raster only; the dependency is called out before install. |
| R20 | The Link maker's copy promises a re-pointable code that static encoding cannot deliver. | medium | Decision D-s; never ship the copy against the behaviour. |
| R21 | A consumer cannot upload anything today (manage-media is workspace-scoped; the chat_* purposes are unreachable by any code). Avatar, biodata photos and emblems all depend on this. | high | manage-media owner scope is a P1 item, before any client photo work. |
| R22 | The ledger is green and stale: check:screens transcribes 2026-08-13 and has no row for twelve designed consumer screens, so the third rule's own instrument understates the gap. | high (process) | Re-transcribe in P0; check:claims picks up the count; every new screen gets a row in the same change as its code. |
| R23 | resolve_slug_status is authenticated-only; Scan and the D5 greeting need an anonymous owner-kind answer. | medium | resolve_slug_owner_kind anon, three-valued, oracle-safe (R9), rate-limited via the EF wrapper. |
| R24 | feature_grants has seven polymorphic scope columns and no XOR CHECK; the biodata caps rely on a rule that is a comment. | medium | Add the CHECK in P1 through the architecture-change protocol (core entity). |
| R25 | Provisioning correctness precedes the feature: primary_context never reaches the server at sign-up (QRS-917, fallback business), accounts are created on OTP send (QRS-998), phones lack + (QRS-936). A biodata owner provisioned as a merchant is the wrong category in every policy. | high | P1 fixes all three before the first biodata row exists. |
| R26 | Stale design premises are load-bearing in three places: email/Google inline registration sheets, MySessions, and consumer.prompt.md's five tabs. A builder reading the artboard alone re-implements the wrong sign-in. | medium | The onboarding registry (rounds 11–14) governs; QRS-912 write-back to the design project; contracts cite the governing artboard. |
| R27 | media_purpose_matches_scope was missed by the first plan (only the XOR was widened); the migration would fail on the second constraint. | medium | Both constraints in §4.1. |
| R28 | Account notification channels imply senders that do not exist (SMS, email). Toggles that persist a preference nothing reads are a promise. | medium | D-p: render absent by availability; only In app and WhatsApp are channels. |
| R29 | delete_account purges a Supabase Storage bucket no migration declares and hard-deletes the auth row; the soft-delete rule is finalised and unimplemented (QRS-909). | high | P1 soft delete with the full cascade; the stray bucket purge removed. |
| R30 | Reserved first segments live in two places (the registry's RESERVED and is_slug_reserved's seed) with no test binding them; intro and photos are new reservations. | medium | One domain constant, a test asserting equality with the SQL seed, both updated in P1. |
| R31 | The centre-button "QR tools" group lists 15 makers and the design opens the create sheet for all of them; eleven have no screen. A grid of eleven dead tiles is the fifth rule's exact failure. | medium | Show only makers with a screen; the rest absent by availability (stated in the contract). |
| R32 | MyProgress has no entry point in the design's own navigation data and its coach read is a merchant screen; building it now creates a floating surface. | low | LATER (D-j), noted in the tracker. |
| R33 | Three retired-vocabulary collisions in the design (two artboard filenames and one store key) would fail check:docs the moment they are transcribed. | low | Product name Intro card, route /intro; QRS-1007 write-back. |
| R34 | get_my_context carries no slug, features or avatar URL; My QR setu and Account need three reads on open. | low | The overview RPC returns identity + slug + biodata rollup in one call; get_my_context unchanged. |
| R35 | Consumer isolation is tested only by granting table privileges first; the production path is RPC-only and untested for cross-user reads. | medium | pgTAP through the RPC surface in P1 (R36 above). |
| R36 | Version and migration: the biodata registry will change (rounds 7–9 added fields); the jsonb bags absorb additions but a renamed field id is a data migration. | low | Field ids are frozen at P1; renames are new ids + a backfill migration; the EF ignores unknown keys but logs them. |
| R37 | Performance: the hub renders 60 fields, 9 sections and a journey on every keystroke in the design. | low | journey() is pure and memoised per section; saves are per sheet, not per keystroke; the overview RPC is one round trip. |
| R38 | Operational: the two utility templates and the Zoom decision are the only external items; both have no SLA. | high (schedule) | Submitted in P0; the plan ships honest-empty if they slip, and says so. |
Verdict. Implementable on the current architecture for P0–P8 with the components named in §2 as EXT/NEW; the two external queues and the confirmations in §12 are the only items that can move the date. R18, R21, R25 and R29 are defects in the previous plan of record and are folded in above.
11 · Phase roadmap, dependencies, acceptance, gates
Sequenced by irreversibility first, then by what unblocks the most. Backend phases run against Dev in parallel with client phases against stub seams.
| Phase | Scope | Depends on | Acceptance (a command produces it) | Gate |
|---|---|---|---|---|
| P0 · Ground | Prod card route 500 (X1) · upload keystore (QRS-908/981) · confirmations §12 · submit the two Meta utility templates · re-transcribe the screen ledger from round 40 (R22) · write the consumer release record | — | curl the production card → 200/404 not 500 · check:screens prints the true unimplemented count · template ids in the registry seed · check:release green | owner sign-off |
| P1 · Irreversibles (backend) | provisioning fixes (R25) · soft delete cascade (R29) · media owner scope + both constraints (R21, R27) · feature_grants XOR via proposal (R24) · biodata schema + reference + templates + grants · shares/requests/kept/reports · notification reads · meetings schema · rate limits · resolve_slug_owner_kind · reserved-segment parity (R30) · pgTAP through the RPC surface (R35) · erasure runbook (R8) | P0 decisions C2, C3 | test:db green incl. negatives · check:sql green · Dev read-back of every object · list_migrations after the first apply · check:arch-proposal green | check:release change records |
| P2 · Seams, schemas, domain ports | packages/schemas biodata/meetings · packages/data seams (stub and real, barrel binds real) · @qrsetu/domain ports for consumer, biodata, meetings, scan, makers · check:rpc allowances retired | P1 | node --test green with both-direction tests · check:rpc 0 new allowances · check:naming | type-check |
| P3 · Chrome, Home, BrandQr | F1 tabs + centre + More sheet + header · BrandQr design pull + drift row · F2 Home round-40 stack against the stub | P2 (stub) | contracts consumer-chrome.json, consumer-home.json re-enumerated, every route row reachableFrom · check:screens built | check:design-parity |
| P4 · Biodata backend + chat write path | manage-biodata all actions · biodata-read · overview/shares RPCs · derivative path (C4) · D3 notice · manage-chat (R18) · missing updated_at triggers | P1, P0 templates | test:ef green · a publish on Dev delivers the notice to a handset · released token reads released, basic never sees private · a consumer sends a text message on Dev and the merchant thread receives it | pgTAP tier tests |
| P5 · Biodata client + identity | F3 My QR setu · F4 editor (10 states, 13 sheets, 60 fields) · F5 people (8 states) · F6 reader (3 roles × 5 states) · share sheet · celebration · Home finished against real data | P3, P4 | contracts my-qr-setu.json, biodata.json, biodata-view.json all rows verdicted · draft survives a kill · device cases on Android and iOS | check:design-parity |
| P6 · Account · Notifications · Scan · Meetings participant · Makers · Chat thread | F11 8 views · F10 kinds + reads · F9 scanner on the shell · F12 calendar/detail/account honest · F13 Wi-Fi + Link · F16 thread with the OTP gate | P1, P3, P4 | contracts per screen · resolve_slug_owner_kind proven on Dev · a seeded meeting is answerable, schedules a local reminder, fetches its link fresh · the 9 design payloads produce the design's verdicts | device cases |
| P7 · Web + cross-cutting | F7 routes + headers + preview + report abuse · D5 greeting · assetlinks.json + intentFilters (after keystore) · i18n three languages (Marathi reviewed) · analytics vocabulary + sink decision · Sentry DSN · default/1 biodata manifest | P4, P0 | run-web asserts noindex, no-store, Cache-Tag · i18n-catalogs.test.ts parity · an Android app link opens the app · check:setu-card-templates covers the biodata manifest | check:docs, e2e |
| P8 · Release readiness | device suite appended and run on Android and iOS · all unit/db/ef/lint/type gates · signed APK + download page · privacy policy, terms, grievance (launch-blocking) · promotion checklist with pasted command output · release record complete | everything | a generated readiness table (screens × contract × journey × open rows) · the six pre-promotion lines each answered by a command | owner go / no-go |
| P9 · Post-release | Meetings hosting (QRS-1014 H1) · chat media/voice/forward · Intro card (D-l) · Photos/UPI/dynamic Link makers (D-d, D-s) · MyProgress (D-j) · co-manager read-only (D-u) · the in-app person-to-person path (D-v, model locked by ADR-0032) · push · marketplace ON + order code · data export delivery | decisions | — | — |
Critical path: P0 templates → P4 → P5 → P8. Parallel tracks: P1/P2/P4 backend beside P3/P5/P6 client against stubs. Can move the date: the two Meta templates, the Zoom decision (P9 only), Marathi review, the keystore.
Release-readiness criteria (all produced by commands, none asserted)
- Every consumer screen in §1 marked LAUNCH has a
builtledger row, a parity contract with every row verdicted, and every route rowreachableFrom;check:screensandcheck:design-paritygreen. test:db,test:ef,npm test,lint,type-check,check:sql,check:rpc,check:naming,check:docs,check:claimsgreen, output pasted.- Device suite cases for every LAUNCH screen pass on Android and iOS, including camera, clipboard, share, picker, local reminder and the app lock over every new route.
- A real family journey on Dev: sign up by WhatsApp OTP → claim address → create a biodata → publish (notice delivered) → mint a person share → open it in a browser (basic) → request → release → read released in the app → withdraw → concluded page; each step observed, not inferred.
run-webprovesnoindex+no-store+Cache-Tagon the built server for both biodata routes and the greeting.- Pre-promotion checklist (sixth rule): declared scope = build; migrations applied on the target both directions; EF set equals the repo's; secrets present by name; client points at the intended project; workflows pass against the target; trace to approved scope by id.
- Privacy policy, terms and grievance route live; report-abuse reachable on every public page.
- The release record (
release.json) carries every change record, targets and builds; approval bound to SHA + manifest hash.
12 · Decisions and confirmations owed (blocking the phase named)
| # | Decision | Recommended | Blocks |
|---|---|---|---|
| C1 | Meetings hosting in the release? | Defer to P9 (H1 on QR Setu's own account, QRS-1014); ship the participant half honest-empty | P6 scope |
| C2 | Biodata URL /<slug>/biodata[/<share>] | Confirm the design's answer | P1 |
| C3 | Removal honoured how | Stop service immediately; 72 h erasure by runbook | P1 |
| C4 | Photograph derivatives | Client-produced pair | P4 |
| D-c | Notification read state | Per account (table) | P1 |
| D-d | Personal UPI maker vs the no-UPI rule (QRS-1009) | Decide; recommended LATER | P6 |
| D-e | PDF / image export of a biodata (QRS-1010) | None, follow the later design | P5 |
| D-f | Biodata URL | = C2 | P1 |
| D-g | Minimum age | AGE_FLOOR 18 as in the design | P4 |
| D-h | Moderation of an emblem or photo | Post-report, remove the emblem never the profile | P4 |
| D-i | expo-clipboard (size callout) | Install | P3 |
| D-j | MyProgress in the release? | LATER (no entry point, coach read is merchant-side, legal copy pending) | P6 |
| D-l | Intro card (personal links page): scope and name | LATER; name Intro card, route /intro, reserve now | P1 (reservation) |
| D-m | Chat scope at launch | Text send/read/state/block/report via manage-chat; media and voice P9 | P4 |
| D-p | Notification channels shown | In app + WhatsApp only; SMS and email absent | P6 |
| D-s | Link maker: static or re-pointable | Static at launch with a design copy correction; dynamic P9 | P6 |
| D-t | Identity greeting and the contact card: does /<slug> ever show the number? | Only when the owner enables it; default off; byte-identical otherwise | P7 |
| D-u | Co-manager read-only (design state) | blocked until a co-manager relationship exists; P9 | P5 |
| D-v | In-app message the family | ✅ DECIDED 2026-09-05 — ADR-0032 locks the identity model. Conversations become principal-based, so a person is a first-class counterparty. R1 still routes this action to WhatsApp on the released number; the in-app path follows the schema change, which lands before manage-chat. | P5 |
| D-w | Consumer prefs storage | Server-side (users.prefs jsonb or consumer_prefs) so phone and web agree | P1 |
| D-x | Re-authenticate (PIN/biometric) before delete account | Yes when a PIN is set | P6 |
| X7 | Mobile analytics sink | Decide, or "instrument now, sink later" | P7 |
Once C1–C4, D-c, D-w and D-l's reservation are answered, P1 starts; the rest fall before the phase they block.
13 · Verification of this plan itself (what to run when it is approved)
bash
# the ledger and contracts this plan will change
npm run check:screens
npm run check:design-parity
npm run check:claims
# the measured baselines quoted in §0
node -e "const j=require('./documentation/portal/design-system/screen-conformance.json');console.log(Object.values(j.screens||j).filter(r=>r.section==='consumer').length)"
grep -rn "manage-chat\|owner_user_id\|resolve_slug_owner_kind" supabase/functions supabase/migrations | wc -l # 0 todayOn approval the plan is written back as the portal plan of record, each LAUNCH screen gains a tracker row and a parity contract, and P0 begins with the ledger re-transcription so that the first measured number the owner sees is the true one.