Appearance
ADR-0033 · The biodata reading has one renderer, and the app embeds it
Status: 🟢 Accepted · Decided: 2026-09-27 (product owner) · Supersedes, for the biodata reading only: ADR-0011 item 3 and ADR-0019's rejection of react-native-webview · Unchanged: ADR-0019 for the Setu Card (its fidelity preview stays expo-web-browser) · Evidence:consumer/biodata-preview-public-parity-rca.md
The decision, in one sentence
A marriage biodata is drawn once, by the web page, and every place that shows it, including the app, shows that page.
What the owner decided
| # | Question | Answer |
|---|---|---|
| D1 | Which presentation is canonical | The web page as it renders in a phone browser. The in-app Preview matches it. A family who scans the QR with QR setu installed opens the app and sees that same page. |
| D2 | Public-only content | Removed: the app strip ("QR setu, in your browser or on your phone" + Download), the grow block ("For someone you love" / "Make one for someone in your family", "Start one, free", "Free to create", "Takes about ten minutes…") and both footnotes ("Kept current by the family…", "Exact date of birth… never shared…"). Kept: QR setu's "Ask on WhatsApp" support card. |
| D3 | How the app shows the page | Embedded inside the app screen (no browser bar): react-native-webview on Android and iOS, an <iframe> on the Web PWA, which has no WebView. |
| D4 | "Report this profile" | Kept, so a misused or fake profile can be acted on. It becomes an approved part of the design, not an implementation extra. |
| D6 | How the embedded page gets the owner's draft (2026-09-27) | The app hands it the draft. A draft that is not published, and the released tier, cannot be read by the page from the database without knowing the owner; the owner chose the road that extends nothing: the app, which holds the record legitimately, cuts it to the previewed tier (projectBiodataEmbedRecord, SQL's clauses) and passes it in. Declined: an owner session or a signed owner link reaching the Worker (QRS-1359, superseded). |
| D5 | Opening the app from a scan | Android App Links and iOS Universal Links for the biodata address. ⏸ Deferred by the owner the same day until the Apple Team ID and a release signing key exist (QRS-1360). Until then a scan opens the web page, and anything that needs the app keeps sending people to the store listing or the app's home. |
Why
Two approved artboards and two renderers produced two experiences, and every check passed, because each surface was validated only against its own artboard (the RCA). Parity maintained by hand between a React Native renderer and a DOM renderer drifts. One renderer makes the Preview, the shared link and the QR scan the same page by construction, and then parity needs checking once, between the page and its artboard.
What it costs, stated so the choice stays informed
The two ADRs this supersedes rejected react-native-webview for these reasons, and each one still applies. It is now paid for deliberately:
| Cost | Consequence | How it is contained |
|---|---|---|
| A new native module | installed builds cannot receive it over the air; new Android and iOS builds are needed | shipped in the same native build as the App Links (D5) |
| No Web PWA implementation | the PWA needs its own <iframe> path, so there are two embed mechanisms | a .web.tsx platform file; the page is identical, only the host differs |
| A new parity seam at the frame boundary | safe-area insets, keyboard, back navigation, scrolling and dark mode must cross the frame | a device pass on Android, iOS and the PWA is part of done (DoD item 5 in the RCA) |
| App size | one more native library | measured before and after, and reported (the app-size budget) |
| Framing a page that takes input (the ask form) | clickjacking if any origin can frame it | in embed mode the page sends frame-ancestors for the app's own origins only |
Consequences
- The native reading blocks are retired (
BiodataViewScreen'sReaderBlocks,AboutBlocks,FamilyDrawing,GrowthCta). The owner band and the tier switch stay native, drawn around the embedded page. A lint rule refuses a native reading block, so a second renderer cannot return quietly. - The web page gains an embed mode (
?embed=1): no site header, and the page leaves the chrome to its host. Content, order and presentation are unchanged, because that is the point. - Parity is checked once, page against artboard, by the presentation gate (QRS-1357). The app side is checked as "shows the same URL, plus only the approved chrome".
- Open-in-app (D5) needs
/.well-known/assetlinks.jsonand/.well-known/apple-app-site-associationserved by the web Worker,intentFiltersandassociatedDomainsinapp.json, and an app route for/<slug>/biodatathat opens the embedded reader.
How the embed works (built 2026-09-27, QRS-1368)
| Piece | Where | What it does |
|---|---|---|
| The contract | @qrsetu/domain biodata/embed.ts | the payload shape, its parser, projectBiodataEmbedRecord, who may frame the page, the embed address |
| The page | apps/web embed/BiodataEmbed.tsx, route ?embed=1 | reads nothing; waits for the payload; draws it with the same resolver and design page a browser gets; hands every outbound link to the host; the language pills stay inside |
| Android and iOS | BiodataViewScreen/BiodataPageFrame.tsx | react-native-webview: the payload injected before load, pushed again on every change and on the page's ready; any navigation away opens with the system |
| Web PWA | BiodataPageFrame.web.tsx | an iframe; the payload posted to the page's own origin only |
Why a page that draws what it is handed is not a forgery surface: it accepts a payload only from the app's own WebView (a global no other site can set) or from a parent frame whose origin is on biodataEmbedParentOrigins (the page's own origin, where the PWA lives, plus the two local previews on a non-production host); the response's frame-ancestors names the same origins, and every other response of the route sends frame-ancestors 'none'; the parser refuses a malformed payload and an unprojected draft. What it does not try to stop is the owner's device rendering the owner's data.
Why a PROJECTED draft and not the whole one: the public read has no "held back" markers, because SQL removes what the tier has not earned. The whole draft would have drawn markers in the Preview that no recipient sees, which is the discrepancy this ADR exists to remove. A test asserts that the projected record reads exactly as the whole draft does, less the markers, at both tiers.
Open, and not decided by this ADR
- iOS Universal Links need the Apple Developer Team ID, which is recorded nowhere in the repo. Android App Links need the signing certificate's SHA-256; release builds currently sign with the debug keystore (decisions.md §5).
- The design still draws the retired native reading (
BiodataView.dc.html), so the Preview's contract rows for the reading areblockeduntil the design re-issues the Preview as owner band plus page (QRS-1368). - Device pass.
react-native-webviewis a native module: a new Android and iOS build, then the frame boundary (insets, back, scrolling, links) on real devices and the PWA.