Skip to content

Consumer architecture — decision record ​

Decided 2026-08-29. This page is the single source of truth for the finalised Consumer direction. Where any other portal page disagrees with it, that page is stale and this one wins.

Why this page exists separately from the research

Everything under /consumer/ other than this page is research — it explores options and argues positions. This page records what was decided, by whom, and what it costs. The distinction matters because research that reads like a decision is how a recommendation quietly becomes an architecture nobody chose. Read Evaluation for the case against moving now; it is still worth reading and its sequencing argument was overruled deliberately, not by omission.

0 · Scope and priority — the reprioritisation that produced this page ​

Ganapati / merchant vendor⏸ Paused until after September 2026
Consumer Marriage Biodata⭐ Top priority, next implementation
Marketplace❌ Not required for Day 1, and not a dependency
Target10 September 2026 (moved from the 5th by the owner)
PlatformsNative Android and iOS — non-negotiable as implementation

⚠ QRS-531 parked this work post-Ganapati and QRS-527 called it "a SECOND PRODUCT that must not enter R1". The owner has lifted that park deliberately on 2026-08-29. Recorded rather than silently overridden, because the reasoning in both rows is still sound and will matter if the schedule slips.

1 · The five architecture decisions ​

D1 · The slug namespace is a registry, not a column on setu_cards ​

/<slug> is an address, and the Setu Card is one of several things that can live at one. ADR-0028 already says this ("a namespace root, not a page"), and the owner has confirmed the root will host a real feature later. So the namespace gets its own table.

sql
create table public.slugs (
  slug         citext primary key,
  owner_kind   text not null check (owner_kind in ('workspace','user')),
  workspace_id uuid references public.workspaces (id) on delete set null,
  user_id      uuid references public.users (id)      on delete set null,  -- NOT cascade
  state        text not null default 'active'
                 check (state in ('active','released')),
  reclaim_hmac bytea,
  claimed_at   timestamptz not null default now(),
  released_at  timestamptz,
  constraint slugs_one_owner check (num_nonnulls(workspace_id, user_id) <= 1)
);

Why a registry rather than widening setu_cards: a consumer identity is not a Setu Card, and putting one in that table would make its name lie and force where workspace_id is not null into every merchant query. It is also not a speculative abstraction (which CLAUDE.md bans): there are two real owner kinds today, and reserved_slugs already exists as a separate governance table, so the schema has already accepted that namespace governance lives outside the card.

The decisive engineering argument: one primary key means a consumer and a merchant cannot collide. Two tables with two unique indexes would need a cross-table uniqueness trigger, which is race-prone and needs advisory locks. One PK removes the class.

⚠ ON DELETE SET NULL, NOT CASCADE — this was a real defect in the first draft

on delete cascade is the house idiom in this schema (reminders, conversations, workspace_members, feature_grants.user_id), so writing it here is the natural mistake. It breaks the owner's rule silently:

users.id references auth.users (id) ON DELETE CASCADE   ← 20260808100000_v2_identity_and_tenancy.sql:62
manage-account/index.ts:113 → auth.admin.deleteUser()   ← a HARD delete
∴ slugs.user_id ON DELETE CASCADE → the slug returns to the pool → someone else claims /priya-sharma

Presence must be permanent; ownership must not be. The row survives the owner.

Three rules the registry must enforce, each derived from an owner decision:

Owner ruleMechanism
A claimed slug is never transferableThe row is never deleted; state='released' instead, and a released slug reports unavailable
Soft deleteSame mechanism. users.status already carries 'deleted' in its CHECK (:79-80) and has zero writers in all 69 migrations — a ready home nobody has used
Realign on return via the same email/mobilereclaim_hmac — an HMAC of the normalised contact under a separately-held pepper. This column cannot be added later: every slug minted before it exists becomes unrecoverable

⚠ A consumer-held slug must report the same status as a reserved one to any availability check. Reporting "taken" moves the account-existence oracle from the route to the RPC rather than closing it. This is a return-vocabulary decision in the registry, not a rate-limiting problem.

D2 · One biodata per account — enforced by entitlement, keyed by subject ​

The owner's rule stands: one QRSETU account holds one active Marriage Biodata. The account is the personal identity, and that matters for the real user-base count and every future consumer feature.

But the schema keys uniqueness on the SUBJECT, and the limit of 1 lives in feature_grants.

sql
create unique index marriage_biodatas_one_active_per_subject
  on public.marriage_biodatas (subject_id) where status <> 'retired';

Same user-visible behaviour today. The difference is that raising the cap later is a config row, not a migration — and it turns the constraint into a revenue lever: a family plan allowing two or three subjects is an obvious paid tier, and a parent with two unmarried daughters becomes a customer instead of a support ticket. A unique (owner_user_id) in the schema would foreclose that permanently and would force the second account the platform's own rules forbid.

The owner's challenge was right and is recorded because it corrected the design: "people are comfortably sharing currently on WhatsApp, then what consent do you want?" A consent gate would be friction against a problem families do not feel, and an earlier draft over-specified it as "the feature's central legal object".

What actually changes is narrower, and it is one thing: a PDF is a copy; a hosted profile is a service we operate. When a father forwards a PDF, Digious is not a party. When he publishes to a QRSETU URL, Digious becomes a data fiduciary for a person who never interacted with us — and since every authorization path resolves through auth.uid(), the subject structurally cannot access, correct or remove her own data.

Decided: at publish, the owner enters the subject's mobile. The subject receives one WhatsApp message naming who created the profile, a link to view it, and a way to ask for removal. No approval, no blocking, no delay for the owner. Skipped entirely when the subject is the account holder. It rides the WhatsApp channel already being built for OTP.

D4 · Photographs — same R2 account and code, private bucket, never the public one ​

A public bucket makes revocation impossible, and revocation is the product's core promise. _shared/r2.ts:15 states the reason in its own header: "A PRESIGNED URL IS A BEARER TOKEN IN A URL. Anyone holding it can use it until it expires."

⚠ Correction to an earlier draft, which cited superseded DDL. media.workspace_id was reported as not null from 20260808170000:87. It has been nullable since 2026-08-14 (20260814100000_v2_chat_media.sql:52), and media_scope_exactly_one already enforces a two-way XOR (:73-74). So the ownership pattern already exists and this is a widening of an existing constraint to three-way (workspace | conversation | owner_user), not a new pattern. The error was grepping create table and missing the later ALTER — see [[verify-alter-not-just-create-table]].

media.bucket already carries check (bucket in ('media','private')), so private storage is modelled in the column. What is missing is wiring: _shared/r2.ts reads a single R2_BUCKET and manage-media/index.ts:247 hardcodes bucket: 'media'. Roughly a day of work, not a schema wave.

D5 · /<slug> renders a soft greeting — identically for every state ​

The root shows a neutral QR Setu greeting today and hosts a real feature later.

⚠ The greeting must be byte-identical for claimed, unclaimed and private slugs. No name, no "this user hasn't set up their page yet", same 200. Otherwise /priya-sharma remains an account-existence oracle over a name dictionary — and because slugs are name-seeded and claimed at onboarding, that is the default state of the largest population on the platform.

Merchant slugs are unaffected and keep their 301 to the public card. A business address is meant to be found; indistinguishability is a consumer-only requirement.

2 · Onboarding — the finalised flow ​

WhatsApp OTP → PIN / Face ID → the consumer surface, with intent established first.

⚠ WhatsApp OTP was decided 2026-08-26, not by this exercise.user-ecosystem.md:548 already records it: Supabase Auth generates, verifies and issues the session; the Send SMS Hook (an Edge Function) delivers via our own Meta Cloud API, so ADR-0029's "Meta direct, no BSP" holds. Supabase's native channel:'whatsapp' is Twilio-only and must not be used.

⚠ ADR-0029 and ADR-0008 are stale against this — both still assign OTP to email (0029:14, 0029:94). Per CLAUDE.md, an ADR is superseded by a banner, never an edit: this needs a new ADR, not a rewrite.

The intent fork is already built and was not recognised as such until measured: WelcomeStory/index.tsx:100-133 forks choose('business' | 'individual'); useOnboardingFlow.ts:11-34 already branches BUSINESS_STEPS (six steps) against INDIVIDUAL_STEPS = ['auth','name','celebrate']; users.primary_context persists it; resolveEntryRoute:40 routes individuals to /consumer.

The one real gap: INDIVIDUAL_STEPS has no slug step, and packages/domain/src/auth/onboarding.ts:55 states that as design intent — "A consumer has nothing to set up: no brand, no industry, no slug, no catalogue." That decision is reversed by D1.

3 · Distribution — organisation accounts only, and neither store opens in the window ​

Legal entityDigious Platforms Private Limited. Organisation developer accounts only. No personal accounts anywhere
D-U-N-SNot yet received. Google Play organisation and Apple both require it
ConsequenceNeither store listing exists on 10 September
Day-1 AndroidA signed release APK served from qrsetu.com, plus /app for anyone who will not install
Day-1 iOSCompiled and device-verified, awaiting enrolment

⚠ QRS-9xx · Every release APK today is signed with the PUBLIC debug key

apps/mobile/android/app/build.gradle:118 is release { signingConfig signingConfigs.debug }, with storePassword 'android' and keyAlias 'androiddebugkey'. Harmless while the store was the only path; a Tier-0 irreversible the moment direct-APK distribution became the launch mechanism.

  1. The signing key is public — anyone can sign an update Android accepts as legitimate.
  2. Android refuses an update whose signature differs, so every Day-1 user must uninstall and reinstall when the Play build arrives.
  3. Play rejects debug-signed uploads outright.

Generate a real upload keystore before the first APK leaves the building.

Parity note: CLAUDE.md's parity standard has no exception path. A consumer product that is native Android + native iOS + /app + a DOM recipient page is a four-surface product, and the recipient page is DOM/SSR by ADR-0019 because it needs OG previews and no-install access.

4 · What is NOT decided, and must not be read as decided ​

  • PIN / Face ID scope. expo-local-authentication is not installed. Two independent plans recommend deferring it to the week of 15 September. It is a local re-auth gate over a persisted session, not a second factor, and it needs an explicit recovery path (re-run the OTP) or a forgotten PIN locks a user out of an account they can still prove they own.
  • The biodata's own URL. D1 settles the identity root. Whether the biodata lives at /<slug>/biodata (mirroring /<slug>/setu-card, and biodata is already a reserved slug) or behind an unguessable token is open, and the two have different privacy properties.
  • Minimum age. DOB is a core biodata field and no age constraint exists anywhere. Publishing age in the open tier means the platform acquires actual knowledge of minors by design.

6 · What Claude Design round 1 settled (2026-08-29) ​

Round 1 ran against the context prompt and produced prototype/my-qrsetu/Direction.dc.html in the design project: direction, IA, navigation, recipient page, disclosure, lifecycle, proactive, and 14 open questions each carrying a default so that nothing blocks.

⚠ Three independent convergences with sections 1-2 above ​

The designer had no access to this page and reached the same conclusions:

Its questionIts defaultMatches
Namespace"One reservation table for people and businesses, permanent, never transferable"D1, the slugs registry
One biodata per account"One profile per subject, and an identity may hold more than one subject"D2, including the two-daughters case
ConsentA logged declaration plus an honoured removal path, not a gateD3

Two designers reaching the same schema independently is the strongest validation D1 and D2 have had. Recorded because a convergence is evidence and an assertion is not.

Adopted from round 1, and NEW relative to sections 1-5 ​

  • Removal is not subject to the owner's veto. The subject's removal request is honoured within a stated window without needing the account holder's approval. D3 gave the subject a removal link and never said the owner could not refuse it. That gap was real.
  • Reopening a concluded search starts with ZERO releases. Nothing old comes back. Matches break and searches reopen; restoring prior releases would silently re-share photographs a family believed were withdrawn. Default release life is 90 days, extendable in one tap, lapsing quietly.
  • A deliberately limited PDF export exists. One page, basic tier only, watermarked with the link and date, no private fields, no full photographs. The reasoning is the part worth keeping: "refusing it is defensible; refusing it quietly is not, because they will screenshot the page instead, which is worse."
  • Four personal kinds are already seeded in the prototype (invitation, birthday card, intro card, contact card) and the QR tools row depends on the contact card. The contact card folds into the identity itself; the other three become visible-and-locked Available capabilities.

⚠ The falsifiable IA test, adopted as a standing check ​

"Adding the second capability should touch no screen above the card, and require no new sharing UI. If it does, the shell leaked into the feature."

The identity shell owns four primitives no capability may re-implement: address, sharing, access, activity. A capability owns only its content and its own lifecycle words. This is the design-side twin of the vendor journey's reuse-badge count: a claim a reader can falsify rather than one they must trust.

Defaults TAKEN rather than decided, and both are reversible ​

One was decided by the owner; one proceeded on the designer's default so round 2 was not blocked.

Q2 — the basic tier carries ONE photograph. DECIDED BY THE OWNER 2026-08-29, overriding both the designer's recommendation and my own agreement with it.

The designer proposed no photograph in basic, flagging it as "the one place I would override the brief and would want to be argued with", and I agreed. The owner argued, and the argument is better: a matrimonial biodata with no photograph does not get opened or forwarded, and the whole growth loop depends on a recipient finding the page worth passing on. A text-only first impression is a worse artefact than the PDF it replaces, which defeats the product.

⚠ The disagreement was narrower than it appeared, because round 1 had already specified the mechanism that makes a basic photograph safe: "before approval, photographs are served small, cropped, and carrying a quiet attribution, from short lived addresses with no direct file link." Resolution gating already existed; the only open point was whether the gated image appears at basic or only after approval. So a basic photograph is a 400px attributed crop on a short-lived URL, which is precisely what "makes a leaked image low value and traceable" was designed for. Full resolution stays behind approval.

Two things do not move, and the first is for a different reason entirely:

  • The WhatsApp link preview carries no photograph. This is cache permanence, not disclosure: WhatsApp caches a preview and revocation can never reach one.
  • The family can remove the basic photograph in one tap and publish a text-only profile. One is the default, not a requirement.

Q6 — free to create, share and conclude. Paid covers what costs us or scales with use: per-person links beyond a handful, activity detail, extra photographs, additional subjects. Never the ending, and never the privacy. Provisional: it does not gate round 2, because the locked-capability states are sequenced after the first five screens.

One correction round 1 made to the prompt, and it was right ​

The prompt described the onboarding intent fork as routing into "entirely different journeys", which reads as permanent. That contradicts this platform's own rule that category is the default experience, never a permanent exclusion. Corrected: one account, one address, both halves available; onboarding chooses the starting journey, not a type. A boutique owner with a daughter of marrying age is both, and the merchant side already has a workspace switcher.

D6 · The profile number is QRS 482 011 735 — nine digits, permuted, never sequential ​

Decided 2026-08-31. A biodata carries a human-readable reference that a family can remember, say aloud to an astrologer, and share on WhatsApp.

DisplayQRS 482 011 735 — nine digits, grouped 3-3-3, the grouping Indian numbers already use
Storedbigint, zero-padded at render. The QRS prefix is a product convention composed at render, never stored
Space10⁹, against a 100M-profile target
GeneratedA keyed Feistel permutation over a bigserial, assigned by the write path

Why a Feistel permutation rather than random-with-retry ​

Random generation into a 10⁹ space holding 100M rows has a load factor of 0.1, so roughly one insert in ten collides and retries, worsening every year. A Feistel permutation is a bijection:

bigserial  ->  keyed format-preserving permutation  ->  9-digit reference
  • Collisions are impossible by construction. No uniqueness probe, no retry loop, no contention.
  • No coordination. The sequence does that, and a cached Postgres sequence is trivially fast here.
  • No order leak. Without the key the output is indistinguishable from random, which answers orders.reference's own objection: "a per-workspace sequence leaks total order volume, so a competitor could measure a rival's business by placing two orders and subtracting."
  • Dense. The whole 10⁹ is usable rather than 10% of it.

Implementation: a 30-bit Feistel (2³⁰ = 1,073,741,824), four rounds, HMAC round function, with cycle-walking when the output lands outside 10⁹. The domain is 93% of the power of two, so the expected number of walks is ~1.07.

⚠ The key lives in the Edge Function, not in the database. A key inside a SQL function body is in git and in pg_proc.prosrc. Assigning the reference on the write path is also exactly what orders.reference already does, so this follows the platform's own precedent rather than inventing a second pattern.

sql
create sequence public.marriage_biodata_ref_seq;

alter table public.marriage_biodatas
  add column reference bigint not null,
  add constraint marriage_biodatas_reference_key unique (reference),
  add constraint marriage_biodatas_reference_range
    check (reference >= 0 and reference < 1000000000);

⚠ Gender is NOT encoded in the number, and the reason is the sharp one ​

A 111 / 222 groom-and-bride prefix was proposed and rejected on 2026-08-31 after debate. Four reasons, and the first is decisive:

  1. It hands an attacker a targeting filter. Random digits give an enumerator a 50/50 mix and mostly wasted effort. 222 means every hit is a bride profile — the population most at risk of harassment, and the reason this feature has a privacy model at all. It halves the search space and aims it.
  2. It welds a correctable field to an immutable one. The requirement is that the number is stable for the profile's lifetime. Encode gender and any correction needs a new number.
  3. It buys nothing at the backend. Matchmaking queries where seeking = ... on an indexed column, and the owner's own constraint was that the prefix must not be load-bearing for partitioning or query performance.
  4. Prefix allocation becomes a taxonomy — a centrally maintained numeric registry, which is the controlled-vocabulary trap already refused for religion and caste.

Instead: gender renders as a chip beside the number (QRS 482 011 735 · Bride), and any future consumer product is distinguished by letters, which are free, never by digits, which are scarce.

⚠ The number is a LABEL until a lookup gates it, and no memorable length is enumeration-proof ​

At 100M profiles in a 10⁹ space the hit rate is 1 in 10; reaching 1 in 1,000 would need eleven digits, which stops being memorable. So the digits are defence in depth and the gate is the control, exactly as every Indian matrimonial service already does it:

  • Lookup by number is authenticated and rate-limited
  • It returns the basic tier only, the same as a shared link. Full detail still needs a release
  • "Findable by number" is an owner switch

Any design claiming enumeration safety from digit count alone at this length is claiming something it cannot deliver.

⚠ Render with a SPACE, never a hyphen. QRS-482011735 would collide with this repo's own QRS-### tracker namespace in every search, commit message and document.

D7 · Versioning reuses the Setu Card mechanism as a SIBLING, not a shared table ​

Decided 2026-08-31. supabase/migrations/20260810150000_v2_setu_card_templates.sql is a complete, working versioning mechanism and biodata copies its shape, not its rows.

What it already provides: (template_key, version) · status in active/deprecated/retired · manifest_schema_version · feature_code as a real FK to features so ADR-0007's gate-by-feature rule is enforced by the database · and the pin living on the record (setu_cards.template_key + template_version).

Three independent locks, all reusable:

  1. Manifests are immutable — superseded, never edited, with a gate validating every version on every commit so an old one cannot silently rot.
  2. setu_card_templates_retire_guard() refuses retirement while any row still pins that version.
  3. The renderer declares which versions it supports.

⚠ COPY THE ABSENT FOREIGN KEY, NOT JUST THE PRESENT COLUMNS

The migration's own header records the experiment that produced this:

"there is NO foreign key from setu_cards to this table… 2026-08-08 is the experiment that already ran — the registry was dropped and every public card kept rendering, because the renderer resolves manifests from the repo. An FK would have converted a metadata outage into a card outage on the platform's only public surface. So: THE REGISTRY GOVERNS SELECTION, THE REPO MANIFEST GOVERNS RENDERING."

Integrity for new selections is enforced at the write path, which is the platform's standing rule that the Edge Function is the primary write-enforcement layer.

A sibling table, not a shared one with a kind discriminator. CLAUDE.md bans abstracting across card types, and it applies here on the merits: a biodata's blocks (the opening, the kundli panel, family layouts, the expectations quote) share almost nothing with a Setu Card's (catalogue, contact, featured). The one piece genuinely worth sharing is the retire-guard trigger function.

And T12 is what makes switching lossless — a manifest block may REFERENCE a field, never CONTAIN content. Because no creator content ever lives inside a template, moving v1 to v2 is one UPDATE of two columns: the number does not move, no data migrates, and every already-shared link keeps resolving. Biodata must hold T12 from the first template or switching becomes a data migration.

sql
alter table public.marriage_biodatas
  add column template_key     text    not null default 'default',
  add column template_version integer not null default 1;

Stable identity + versioned presentation, enforced by the mechanism already protecting live merchant cards.

D8 · Authentication is WhatsApp OTP primary, Google a PERMANENT fallback, and email never ​

Owner decision, 2026-08-31, final. Two options and only two:

OptionRole
1WhatsApp OTPPrimary and preferred, on both audiences
2Google social loginFallback, offered permanently and to new accounts too
—Email OTP, email magic linkNever. Not now, not later, not as a third option

Why WhatsApp is primary is a commercial reason, not a convenience one. It captures the mobile number at the first step, which is what makes later communication, engagement, support and marketing possible at all. A signup that yields no number yields no relationship.

Why email is excluded outright. It was removed in design round 12 and does not return. Beyond the owner's preference there is a measured reason: merchant email OTP is dead in production — Supabase returns 535 authentication failed, because custom SMTP points at Hostinger while the domain's verified transactional sender is ZeptoMail (QRS-285). A third channel would also be a third thing to keep working, on a two-person team, for no audience that the other two miss.

⚠ This SUPERSEDES the removal clause in QRS-926 ​

That row recorded Google as a legacy door on a countdown, to be removed once select count(*) from auth.users where phone is null reached 0. There is no countdown any more. Google is a standing second option, so the copy changes with it: round 12's "Signed up with Google before?" reads as a door for old accounts and is wrong. It becomes an ordinary second choice, quieter than the primary but not historical.

Two things get better because of this, and one gets harder.

  • ✅ QRS-919 stops being a launch blocker. Meta's authentication-template approval is a queue with no SLA, and it was the largest schedule risk to 10 September precisely because nothing else could sign anybody in. With Google standing permanently, a slow Meta queue delays the preferred path rather than the only path. The provider-agnostic phone-OTP interface is still the right build — it is what lets SMS carry the primary path meanwhile — but the release no longer hangs on an external approval.
  • ✅ QRS-910 stops needing a migration. All seven existing accounts are Google-only with zero phones; keeping Google means they were never at risk and no backfill is owed.
  • ⚠ Apple Guideline 4.8 becomes a standing question rather than a temporary one. An app using a third-party login service to establish the primary account must also offer an equivalent option meeting specific privacy criteria. Whether our own phone-OTP path satisfies that is genuinely unresolved — the guideline's equivalent-option criteria are written around name and email, and we collect a phone number. This is recorded as unknown rather than answered, per the third rule; it needs checking against the current guideline text before the iOS build is submitted, not assumed either way.

The consequence that is easy to miss: Google does not give us the number ​

The entire reason WhatsApp is preferred is the number, and signInWithOAuth returns none — the provider owns the metadata and Google does not share a phone. So a Google fallback that ends at sign-in silently discards the one asset the primary path exists to collect.

Therefore the number is still asked for after a Google sign-in, as its own step, before the landing. Two readings, and the second is the one that must not be dressed up:

  • Ordinary: they preferred Google, we still want a number, one line says why.
  • Degraded: if the reason they are on Google is that no code could be delivered, then the number cannot be verified right now either. Accept it, flag it unverified, and say so plainly. An unverified number recorded as verified is worse than no number, because every later message, recovery path and support call trusts it.

Sent to Claude Design as round 13.

D9 · The biodata reading has one renderer, and the app embeds it (2026-09-27) ​

Decided by the owner after the root-cause analysis (biodata-preview-public-parity-rca.md), recorded in full in ADR-0033.

  • Canonical: the web page as it renders in a phone browser. The in-app Preview and a family who scans the QR with the app installed see that same page.
  • Removed from the page: the app strip with Download, the grow block ("Make one for someone in your family", "Start one, free", "Free to create", "Takes about ten minutes"), and both footnotes.
  • Kept: "Ask on WhatsApp", QR setu's support card, and "Report this profile", now approved.
  • The app embeds the page (react-native-webview; an <iframe> on the PWA). ⚠ This reverses the earlier rejection of react-native-webview for this reading only, knowingly: it needs new native builds and a device pass.
  • A scan opens the app (App Links and Universal Links). ⏸ Deferred the same day until the owner has the Apple Team ID and a release signing key (QRS-1360); until then the scan opens the web page and app-only actions go to the store listing or the app's home, as today.

D10 · MVP: no request-access hurdle; the ID is D6's; zoom and a 3 s slider stay (2026-09-28) ​

Decided by the owner on 2026-09-28, answering the four questions of the design-to-implementation audit, verbatim in substance:

  1. No "Request more details" / "Request access" for the MVP. "The biodata should directly render all information that the user has explicitly allowed to be shown publicly … park the advanced access-request/privacy workflow for a future phase." So the ask form, the code step, the waiting request and releasing a request are out of MVP scope (QRS-1376..1379, QRS-1381 parked), and the form must not render at all.
    • ✅ ANSWERED 2026-09-28 (owner): reading (a). "All the fields those toggled enabled are meant to be publicly rendered." Everything the family has not made private or hidden (today's basic and released fields, and every shown photograph) renders on every link, to every reader. Rejected: (b), basic fields only. This changes the public read (SQL), the page's tier logic, the photo rule (all shown, not cover only), the People and access screen (releases, lapse dates and person links are moot for the MVP) and the editor's tier copy.
  2. The Biodata ID is D6's format: QRS 482 011 735, nine digits (QRS-1393). The minted 7-character codes on Dev (3G5FH0X) are replaced; Dev only, before launch, so no family holds one.
  3. Zoom stays in the photo viewer as built (1x, 1.8x, 2.5x), a recorded divergence from round 57, which draws none (QRS-1395).
  4. The photo auto-slider stays at 3 s (QRS-1371), against the design's 4.2 s.

D11 · The MVP public read, the 9-digit ID on Dev, the scan, and two screens hidden (2026-09-28) ​

Decided by the owner on 2026-09-28, following D10 (a):

  1. The public read serves every reader basic and released content, and up to four shown photographs with zoom, on Dev. Accepted knowingly: a printed QR code now shows the full name, both parents' names, the shown address and map place, references, employer, LinkedIn, place of birth and the family contact's phone. private fields (date of birth, phone number, income, address) and anything hidden stay out. The in-app Preview projects the same tier, so the two cannot differ.
  2. The Biodata ID moves to D6's 9-digit number on Dev (a number column, the Feistel mint kept, existing Dev IDs re-issued). Where it appears on screen is a design request.
  3. A scan of another family's biodata opens their web page. Later, when QR setu is installed, the scan should open it in the app instead, and stay in the browser otherwise (the D9 App Links and Universal Links item, QRS-1360).
  4. People and access and the Preview's reader switch are hidden for the MVP. The Preview shows the one public page with the owner's Edit. Both return with the request phase (D10).

5 · Tracker rows owed ​

RowWhat
Android release signing uses the public debug keystore§3 above — blocks direct-APK distribution
manage-account hard-deletes the auth rowContradicts the soft-delete rule today; users.status has zero writers
Seven Google-only identities have no auth pathauth.identities: google=7, 0 of 7 have a password. Removing Google locks out every account on Dev
Production serves HTTP 500 on the card routeqrsetu.com/<slug>/setu-card → 500 while devv → 404. Stale build plus, most likely, missing Worker vars