Skip to content

Implementation readiness ​

⚠ THIS IS A LOG. IT RECORDS WHAT WAS TRUE ON 2026-08-31, AGAINST DESIGN ROUNDS 3 TO 10.

Do not edit it to match a later round. Run a new one. An assessment edited forward loses the only thing it was for: a record of what was known when the decision was taken.

Audited 2026-08-31. Design state read live from the Claude Design prototype project (never a local cache). Repo state measured from apps/mobile, packages/data, packages/domain, supabase/, and apps/mobile/package.json — by reading the files, not by reading earlier documents about them.

What this audit means by "blocker", because the word does most of the work ​

A blocker is something that must be DECIDED or ACQUIRED before code can start. Planned build work is not a blocker, however large.

marriage_biodatas does not exist, and that is not on the list below. Neither is the editor, the public page or the access model. They are the build. What is on the list is the handful of things where a developer sitting down on day one would either stop, or guess — and a guess here is expensive, because each one is load-bearing for something already designed.

ClassCountMeaning
🔴 Implementation Blocker6 open, 1 resolved todayMust fix before development. A decision we do not have, or a dependency we do not own.
🟠 Important Gap9Should fix before development. Building without them is possible; shipping without them is not, or costs rework.
🔵 Enhancement9Can be iterated later. Genuinely optional for R1.

The headline, stated plainly: the design is in good shape and the blockers are mostly not engineering. Four of the six are a decision or an external queue. Only two are code.


🔴 Implementation Blockers ​

B1 · The slugs registry does not exist, and every address in the feature hangs off it ​

D1 specifies it; nothing is built. Every biodata URL is qrsetu.com/<slug>/biodata/..., the identity QR encodes it, and the share link carries it. Without the table there is no address, no QR and no share.

⚠ And the design has already hit the consequence. qr-registry.js's setu_card matcher claims any first segment, so a person's slug resolves as a shop (QRS-914). Ordering the new matchers before it fixes two of three cases; the third — a shop and a person have identical URL shapes — is unsolvable in a matcher and needs owner_kind, which is exactly what D1 carries.

Resolution: it is Phase 1.1 and it is the first migration. Nothing else in the feature can be addressed until it lands.

B2 · WhatsApp OTP has no implementation path, and its critical path is a queue we do not control ​

QRS-919. AuthService exposes sendEmailOtp / verifyOtp and nothing phone-shaped. Supabase phone OTP needs an SMS provider or a Send SMS Hook; ADR-0029 chooses Meta Cloud API direct.

The code is roughly a day. Meta business verification plus authentication-template approval is not, has no SLA, and shares its documentation dependency with the outstanding D-U-N-S — so the two queues are correlated rather than independent.

Resolution, and it must be taken now rather than discovered in September: build against a provider-agnostic phone-OTP interface. The delivery channel becomes a swap instead of a rewrite. If Meta has not cleared when the feature is otherwise ready, launch on SMS through the same interface and move to WhatsApp on approval. The person sees a six-digit code either way. Start the Meta application in parallel, today.

B3 · The moderation policy is undecided, and it gates the emblem upload screen ​

Round 8 flagged it and it is still open: is review pre-publication or post-report, who reviews, and what happens to the profile when an emblem is removed. The upload screen cannot be built without knowing — "your emblem is live" and "your emblem is being reviewed" are different screens, different states and a different data model.

Recommendation, for an owner decision: post-report review, uploads live immediately, and removing an emblem removes the emblem and never the profile (they are unrelated, and destroying a family's whole biodata over a picture is a punishment nobody designed). Two reasons: a two-person team cannot staff a pre-publication queue by 10 September, and a biodata is shared point-to-point by link rather than published to a feed, so the blast radius of a bad emblem is the family's own recipients rather than the public.

⚠ QRS-922 shrinks this considerably but does not remove it — dropping the named collection removes the gallery, and a family's own uploaded picture is still user content on a page a stranger opens.

B4 · The media pipeline decision: tier-gated photographs cannot be built the way they are drawn ​

QRS-920 + QRS-923. Two findings, one pipeline, so they must be decided together or the upload path gets built twice.

  • originSupportsTransforms() is false for pub-*.r2.dev, which is exactly what Dev points at. So the central privacy mechanism — basic tier sees a cropped, watermarked, low-resolution photograph; released tier sees the original — is undevelopable in the only environment that informs design.
  • And the obvious fix is wrong anyway. Gating by URL transform parameters is not an access control: if the full-resolution object is reachable, editing the URL reaches it.
  • Screenshot prevention does not exist on two of three surfaces. Android's FLAG_SECURE works; iOS offers no API to block a screenshot at all; and the public page is a browser document where nothing can be blocked — which is the surface strangers actually receive.

Resolution: bake the derivatives at upload. manage-media writes two private objects — a watermarked, downscaled basic derivative and the untouched original — and mints a short-lived signed URL for whichever the caller's tier entitles. This removes the transform dependency, behaves identically on Dev and production, and burns the watermark into the bytes so it survives every screenshot and cannot be stripped by URL manipulation, which a render-time overlay never does.

⚠ The copy is the half that must not go wrong. CLAUDE.md's never volunteer a privacy reassurance at the point of use rule applies at full force. State what the product does — the reader's own name is on every copy — never what it prevents. A family shares more widely on the strength of a guarantee, so a false one is worse than silence.

B5 · There is no rate limiting anywhere, and OTP is metered ​

QRS-921. CLAUDE.md has required it since the standards programme began and nothing implements it. It has been survivable because merchant sign-in is low volume and the public card is a cached read.

The consumer feature changes the shape of the exposure three ways at once: an OTP send costs real money per message, so an unthrottled endpoint is a direct drain and a provider policy violation; the access-request path is unauthenticated by design and writes a row; and the address space is enumerable in principle, so an unlimited reader is a scraper.

The OTP endpoint cannot ship without it, and it needs two independent limits — per phone number and per source — because either alone is trivially bypassed.

B6 · The onboarding fork's account type never reaches the server ​

QRS-917. sendEmailOtp(email, primaryContext?) accepts it and useEmailAuth omits it, so handle_new_user applies its 'business' default for every signup. OnboardingSetup corrects it at the celebrate step — so a consumer who verifies their code and then closes the app, the most common abandonment point in any OTP flow, is recorded as a merchant permanently and is routed into merchant onboarding on every later launch.

Deliberately not fixed in place, because useEmailAuth and AuthScreen are precisely the files B2's rebuild replaces. Specified as a requirement of that rebuild, where it is the sharper requirement: whether signInWithOtp({ phone }) writes raw_user_meta_data the way the email path does is itself unverified (QRS-910), and if it does not, every consumer is provisioned as a merchant. Verify before building.

B7 · ✅ RESOLVED 2026-08-31 — a returning consumer was sent to the merchant console ​

QRS-918. AuthStep routed on session.onboardingCompleted, which short-circuits to true for every individual without counting workspaces — so that branch was the consumer's path as much as a returning merchant's, and it replaced to /dashboard unconditionally. Reachable on the ordinary reinstall walk.

⚠ The root cause is worth carrying forward: AuthSession had no category on it at all, so no test written against that surface could have told the two accounts apart. Its own fixture described a consumer exactly as accurately as the merchant it was named for. Adding the field is what made the missing case expressible.

Fixed by putting accountType on the session (built where ctx already was), branching on it, and persisting it in sessionStore.signIn — which held the authoritative answer and discarded it. Mutation-tested both directions; type-check clean; 159 tests over 20 suites green.


🟠 Important Gaps ​

#GapWhy it mattersRecommendation
G1@qrsetu/i18n ships English only; the design is Marathi-first (QRS-924)The launch market is Maharashtra and the reference competitor is a Marathi generator. An English-only biodata is not a reduced product, it is the wrong one. Launch-blocking, not development-blocking — the strings come from a catalogue either way.Ship mr + en, defer hi. The design's LABELS.ui is already written for mr, so this is transcription plus review. Carry NON_TRANSLATIONS with it or a translator will silently "correct" the eight template words back into our English.
G2PIN / Face ID: expo-local-authentication is not installedThe required flow is WhatsApp OTP → PIN/Face ID → landing. A new native module also owes a size callout before install (CLAUDE.md).The PIN is a device gate, not an onboarding step — see the onboarding assessment. Ship PIN on expo-secure-store (already a dependency, zero new weight); add biometrics via expo-local-authentication as an additive unlock.
G3The identity greeting page is not builtD5 requires /<slug> to render byte-identical for claimed, unclaimed and private. That is not cosmetic: a 404-versus-greeting difference is an enumeration oracle.Build it with the slugs migration (B1), not after. It is small and it is the privacy control.
G4No push channelexpo-notifications is installed; there is no token table, no send path, no FCM/APNs credentials. Access requests and releases need to reach the owner.WhatsApp is the R1 channel — D3 already sends one notification with a control link. Push is R2.
G5consumer.prompt.md is stale (QRS-912)It is the persistent context every future consumer prompt inherits, and it still defines a consumer as consumption-only with no identity, and names email/Google as the auth channel.Amend before the next design round, or that round inherits both errors silently. Needs a finalize_plan.
G6The person model is still a delimited string in the repo's target schema (QRS-915)Round 9 fixed it in the design (values.people as objects). The repo must land family_members jsonb rather than re-implementing the · split.Phase 1.12. Cheapest now, most expensive after real families have typed into it.
G7Photograph counts are not plan-gatedFlagged by the design itself. Monetization is out of scope for this pass, so no cap exists anywhere.Decide the cap with pricing, not with the build. Leave the field, add the guard later.
G8A released family loses everything with no explanation when the subject asks for removalThe design's own open question 2. Correct for the subject, harsh for a family mid-conversation.A one-line state on the lapsed/withdrawn page. Cheap, and it prevents the worst message a recipient can receive: silence.
G9biodata_templates versioning (D7)Retrofitting versioning once templates exist is a migration.Phase 1.11, in the first wave. Copy setu_card_templates' shape including its deliberately absent FK.

🔵 Enhancements ​

Genuinely later, and listed so nobody mistakes an omission for an oversight: kundali or horoscope as an attachment · a per-recipient note · photograph captions · a choice of print layout for the PDF keepsake · real getComputedTextLength measurement in the SVG family drawing (the current wrap budget is a scaled character count) · scripts beyond Devanagari and Latin (Gurmukhi and Arabic first) · the post-launch devak promotion decision · giving a withdrawn link something to do · adopting consumerBarTabs() in the two screens that inline the same split.


The emblem opening: the alternative ​

Ten rounds is enough evidence. om survives at both sizes in both themes; Ganesha and Swami Samarth were drawn and pulled. The design's own conclusion is the right one and it is not a proportions bug: figures built from ellipses and single bezier curves read as a rounded mascot, and that is what the construction method produces for a figure. A symbol carries no anatomy, which is exactly why it survived where the two figures did not. More iterations buy nothing.

R1 opening = words · their own picture · an ornamental frame · nothing at all. Full reasoning and the four problems it removes: QRS-922.

⚠ Read it as a better answer rather than a cheaper one. The transcribed Marathi template — the market's own artifact — opens with a centred image and ॥ श्री गणेशाय नमः ॥ beneath it, and the words are the invocation while the image is decoration. The text route is already built and the design calls it the best-looking opening in the prototype. Removing the named collection removes the per-file licensing burden, the moderation surface a gallery adds, the taxonomy argument round 8 already refused, and the uneven-coverage problem the design flagged against itself.

EMBLEMS, art and the collection route stay in the model, so commissioned figures drop in later as data with no schema change. That is what the compositional model was for. Commissioning is post-launch and off the critical path.


What this audit cannot see ​

Stated so a clean bill is never over-read, in the same terms every gate in this repo states its own limits:

  • Whether the enumeration is complete. This is a human read of ten design rounds and one repo. It finds what it looked at. check:design-parity gates the honesty of a contract, never its completeness, and the same applies here.
  • Whether the designs are visually right. No script and no reading judges spacing, hierarchy or whether a family will trust the page. That stays a screenshot and a human.
  • Whether the estimates hold. Nothing here is a schedule. B2's queue in particular is owned by Meta and no amount of preparation on our side shortens it.
  • Anything about production. Every measurement above is against the repo and Dev. QRS-911 is the standing reminder that nothing in CI observes production at all.