ADR 034 Mother Account Identity, Profile, and Consent Authority
Context
Section titled “Context”Legacy signup accepts email, name, phone, and password, then creates its own credential token. Email duplication is visible before signup, required consent is only a browser checkbox, and no accepted policy version or content digest is kept. Social login trusts a caller-supplied provider token. General accounts can be reported complete with no birth date or address, while the address write can fail and the endpoint still returns success.
The current migration has a second authority defect. An authenticated Supabase
client can directly insert or update its row in sanmopia_member_profiles, including
member_role, member_id, and branch_profile_id. Reservation actor resolution then
trusts that mutable role. P03 cannot pass, and P04 cannot open, until direct profile
authority is closed.
Decision
Section titled “Decision”Native identity first
Section titled “Native identity first”Supabase Auth is the only owner of passwords, provider identities, email or phone verification, access and refresh sessions, and session revocation. The frontend uses the native Supabase signup, OTP, MFA, and identity-linking surfaces through a named facade. Sanmopia does not implement its own password hash, provider-token verifier, or OTP service.
POST /mother/accounts runs only after native Auth has issued a verified bearer
session. The request never accepts a password, OTP, provider token, access token,
refresh token, user id, member id, role, branch id, profile id, lifecycle, or
verification state. The backend resolves the bearer subject and re-reads verified
contact facts from Supabase Auth. Missing, invalid, expired, or withdrawn sessions
are 401; unverified contact is a closed typed 422.
Member Management account authority
Section titled “Member Management account authority”member_management / mother_account_profile owns the mother account lifecycle,
profile completeness, revision, structured address revision, accepted consent
receipts, current marketing preference, idempotency result, and privacy-safe audit.
It assigns member_role=mother and every internal identifier on the server.
The first completion command requires:
- one verified Auth contact fact;
- canonical name and non-future birth date;
- one complete structured postal address;
- the exact published required policy ids, versions, and content digests; and
expectedRevisionplusIdempotency-Key.
Profile, address, frozen verification receipt, consent receipts, command result, audit metadata, and the relationship outbox message commit in one Postgres transaction. Passwords, OTPs, provider tokens, raw bearer tokens, and raw PII snapshots never enter command fingerprints, audits, logs, or evidence.
The same subject, key, and semantic payload returns the accepted identifiers and
projection without another row, revision, consent receipt, audit, or relationship
message. Reusing a key with different semantics is a typed 409. A stale revision
is a typed 409 with only an owner-safe latest projection and field diff. A policy
replaced between display and submission is a stale-policy conflict; the server never
silently accepts a newer document.
Closed database and relationship authority
Section titled “Closed database and relationship authority”authenticated and anon receive no direct insert, update, or delete authority on
the generic member profile or P03 tables. Self-service writes pass through one
service-role RPC that derives immutable scope from the verified bearer subject.
RLS remains defense in depth for self reads and denies cross-member raw data access.
SpiceDB gains sanmopia/mother_profile. Its owner relation grants only self view
and update. The Postgres transaction queues
mother_profile:{profileId}#owner@sanmopia/user:{subject}. The account remains
authorization_pending until a matching application receipt and permission check
exist; the product must not claim an active, complete account while delivery is
unknown or failed.
Privacy lifecycle
Section titled “Privacy lifecycle”Withdrawal is a credential-confirmed public command owned by the existing privacy lifecycle through an explicit port, not a deep feature import. It revokes sessions, masks or purges profile PII, removes the mother-profile owner relationship, and applies an approved retention policy to consent proof. Operational rows, pending repair work, and queued relationship mutations must reach zero for a test-owned account. Legal retention is purpose-specific; transaction retention periods are not automatically assigned to signup consent.
Source-record retention and expiry authority
Section titled “Source-record retention and expiry authority”Retention is decided per source record, never once per member or once per broad
category. member_management owns the lifecycle ledger and orchestration contract;
the bounded context that owns each source owns its selector, approved retained field
set, immediate operational redaction, source revision/digest, and expiry eraser.
Cross-context calls use application ports. They do not import sibling adapters.
| Source owner | Source record | Basis and duration | Server-owned anchor |
|---|---|---|---|
reservation_operations | one reservation booking and its approved contract evidence | Electronic Commerce Act contract/withdrawal record, five years | closed service/contract date; an open reservation is a blocker |
pricing_settlement | one deposited payment and approved payment/supply evidence | Electronic Commerce Act payment/supply record, five years | deposit or receipt-recorded date, whichever is later |
pricing_settlement | one finalized reservation financial lifecycle and approved ledger evidence | Framework Act on National Taxes article 85-3, five years | explicit legal filing deadline; missing deadline is a blocker and is never inferred from withdrawal time |
care_delivery | one daily care report | Electronic Commerce Act supply evidence, or social-service provision evidence for voucher service, five years | service date |
reservation_operations | one voucher recipient eligibility/provision record | Social Service Use Voucher record retention, five years | related service date |
| complaint owner | one complaint or dispute case | Electronic Commerce Act complaint/dispute record, three years | resolved date; unresolved or unavailable complaint authority is a blocker |
All business dates use Asia/Seoul. retentionUntil is inclusive. Purge eligibility
starts on the following Korean calendar day. A timestamp anchor is converted to its
Korean local date before duration arithmetic. Leap-day anniversaries clamp to the last
valid day of the target month.
Separation copies only the approved legal evidence fields into a service-role-only source-record ledger, records source table/key/revision/digest, and redacts unnecessary PII from the operational source in the same transaction. The ledger key includes lifecycle, source context, source record, category, and basis, so two reservations or two payments never collapse into one marker. A payload digest or source revision change after separation is child-record drift and closes purge until reconciled.
Purge tasks are grouped by one inclusive expiry date and link explicit source-record
items. Claim uses FOR UPDATE SKIP LOCKED, a lease token, attempt counter, and lease
expiry. Completion requires one immutable receipt per source record. Exact replay is
non-mutating. Expired ledger payloads are removed while audit keys, legal basis,
source digest, disposition status, and purge receipt remain. Non-expired records in the
same lifecycle survive unchanged.
An FK-closure catalog is frozen with the migration. A later source/child FK absent from that catalog makes the coverage doctor fail closed until its owner declares the retention and erasure disposition. Generic marker JSON replacement is not accepted as source erasure evidence.
Review-before-effects and atomic completion
Section titled “Review-before-effects and atomic completion”An account that requires operator review stops before source separation, purge
scheduling, Auth masking, profile masking, or feature-owned erasure. A review signal is
not completion authority. The authorized reviewer command accepts only withdrawal
receipt identity, expected review revision, the closed
blocking_facts_resolved code, and an idempotency key. Actor, role, branch, source
facts, PII, retention policy, note, and reason are server-owned or forbidden.
After the accepted signal, Restate applies each irreversible effect as its own durable
step through the source-owner application ports. A dedicated database RPC then locks
the accepted review, signal, original privacy lifecycle, withdrawal request, profile
erasure, separation receipts, and purge schedule. It consumes the signal, completes
the original lifecycle and request, and inserts one immutable review-completion receipt
in the same transaction. Exact replay returns that receipt. A completed reviewed
lifecycle projects completed before the historical review flag is considered.
Purge execution distinguishes terminal authority failures from transport uncertainty. Only a database-authoritative failure such as source-record drift stops the workflow. Network, timeout, and malformed/missing transport responses bubble to Restate retry; they are never converted into a terminal business failure.
Boundaries
Section titled “Boundaries”- Contract owns strict POST, self GET/PATCH, result, and problem schemas.
- Backend code is Layer-first, then
member_management/features/mother_account_profile. - CQRS folders exist only inside that application feature because the feature has real commands and a real self query.
- Sibling features, reservation code, and privacy lifecycle collaborate only through public ports or contracts. Direct implementation imports are forbidden.
- Frontend computes no lifecycle, completeness, verification, or permission truth.
- The admin review surface sends only the closed review intent and uses the shared TS contract. The server action resolves the HttpOnly-cookie bearer session; the browser cannot select reviewer identity or authority scope.
- Assembly owns Auth policy, stage fixtures, the actual six-case machine actor runner, scoped browser evidence, cleanup, immutable four-repository pins, screenshots, WebP, and Starlight proof.
- P04 remains blocked until P03 is actual PASS.
Required Evidence
Section titled “Required Evidence”Implementation can advance only with all six P03 cases against one clean pinned revision set: happy path, authorization, validation, conflict, exact replay, and privacy cleanup. Evidence must prove native verified identity; one profile and address revision; exact consent receipts; owner relationship application; zero rejected-path mutation; direct Data API role escalation denial; session revocation; PII and relation cleanup; desktop/mobile screenshots; a real-transition WebP; and redaction review.
Implementation Evidence
Section titled “Implementation Evidence”The v2 partial machine pack at
docs/evidence/p03-mother-account-profile/partial-pack.json locks one clean-pinned
same-composition actor run, its primary-state manifest, the historical browser source,
the withdrawal-only browser run, cleanup, privacy review, and the separate visual
classes. The machine actor uses local Supabase Auth and PostgREST, disposable SpiceDB,
and disposable actual Restate. For one fresh account with no prior business history it
proves all six machine cases:
- happy path returns registration/profile/withdrawal HTTP
201/201/202, completes Restate, and applies then removes the owner relationship; - authorization and validation return
401/422with no primary-state mutation; - replay payload conflict returns
409with no primary-state mutation; - registration, profile update, and withdrawal exact replays each return
200without changing their primary state; and - wrapper cleanup leaves Auth, operational, relationship, container, process, and temporary-directory residue at zero.
primary-state-manifest.json locks the same actor digest and records profile revision
1 → 2 → 3, address revisions [1] → [1,2] → [], two consent receipts, removed verified
contacts, completed withdrawal/privacy workflow, and the relationship delete receipt.
The manifests record no raw credential, PII, actor identifier, request/response body,
private topology, or idempotency key. caseSetPassed is true while completionClaim
remains false.
The v2 pins are assembly 9cab9b24a8ed35bfc647b0e2b945a0f271d3f5dc, contract
dc3c210cb3ca0d37bc1a55bd590258964daf22ec, backend
0a126784232f4d1e805ce3357a49079f1fc96fed, and frontend
fbed8f864542ddd26d6756e368a003795c669adf. Every repository record also locks its tree
and clean: true.
Frontend commit bdb8d7cc2a8f19bf14470157db8718e3474b650e supplies UI contract captures for
join, editable profile cards, two-step withdrawal confirmation, and the result card.
Those assets are synthetic fixture rendering. They do not prove actual browser network,
submit, reload, pageshow, Supabase state, SpiceDB state, or cleanup behavior. The
classification is enforced by the P03 evidence gate and rendered explicitly in
P03 partial A-Z evidence.
A separate actual browser run uses clean frontend commit
1a314bc6a7bc09a7518bba819079c4abe99fb285, a hashed served Astro build, actual local
Supabase, disposable SpiceDB and Restate services, and Chromium. It proves the body-free
withdrawal reaches terminal authority, same-key replay returns HTTP 200, and full
reload, pageshow, and astro:page-load recover the withdrawn result without horizontal
overflow or browser diagnostics. Five redacted actual-browser WebP assets are kept apart
from the six synthetic UI contract assets.
That browser manifest points to the byte-preserved
actual-browser-source-actor-run.json, not to the newer six-case actor manifest. The
browser proof is withdrawal-only. It does not claim browser coverage for authorization,
validation, conflict, registration, or profile mutation.
The full reload is an actual browser navigation. The pageshow and astro:page-load
checks are manually dispatched inside actual Chromium and prove handler idempotency; they
are not claimed as naturally emitted ClientRouter lifecycle events.
The live Restate run also found a time-unit defect. WorkflowContext.time() returned an
epoch seconds float. Dividing it by 1000 produced a 1970-era transition timestamp that
the database correctly rejected as preceding the request. Removing /1000 and using
datetime.fromtimestamp(await ctx.time(), timezone.utc) made a fresh network invocation
complete successfully. The reload run then exposed a second defect: after Auth masking,
SSR rendered an auth-required fallback with no profile controller, so pending storage
could not recover. The fixed recovery surface performs the exact body-free same-key
replay, requires matching receipt and request time before promoting storage, hides the
fallback, and preserves terminal state across page lifecycle events.
Backend pin ed8174cadcf03774e27631d7d7d31aff566fcc1f implements the historical-review
completion authority and the reservation, pricing/settlement, and care-delivery
source-owner adapters. The fresh Supabase gate passes review resumption, atomic review
completion, per-source retention, purge, source drift, claim race, replay,
authorization, and cleanup. The full backend suite reports 6667 passed; Ruff and Tach
are green. Complaint/dispute remains fail-closed because its owner authority belongs to
P04.
Contract pin c4b2488a5b93081d9da9f03d17fdc1733a64ca77 publishes the strict TS runtime
contract and passes all 484 contract tests. Frontend pin
23d999a3ea234608463147e1e2d90b712bca0d8a adds the Material navigation-rail
destination, closed server action, client validation, responsive review form, and
unauthenticated fail-closed behavior. Astro check/build and frontend lint pass; the
complete 569-test frontend set passes after updating the navigation destination
cardinality.
The provisional actual-browser pack at
/evidence/p03-admin-withdrawal-review-provisional/ records desktop 1280px, mobile
390px, Material rail/app-bar/drawer switching, horizontal overflow 0, console
error/warning 0, and unauthenticated authentication_required with no result card.
Its animated WebP has three frames. It has no authenticated HQ reviewer and therefore
does not promote P03 or prove successful completion/purge.
P03 completion evidence
Section titled “P03 completion evidence”The canonical complete pack at
docs/evidence/p03-mother-account-profile-complete/api-manifest.json closes
p03-history-operator-resume, p03-source-retention-erasure, and
p03-retained-record-purge. An actual historical mother, authenticated HQ reviewer,
and caregiver denial actor passed the waiting, authorization, validation, blocker,
resumption, exact replay, source separation, expiry, and purge cases. The same run
proved six retained-source records across reservation, pricing/settlement, and
care-delivery, six purge receipts, non-mutating Restate replay, and teardown residue 0.
Frontend 864c78e and backend a9dd6a7 add a test-only browser handoff. Actual
Chromium submitted the HQ review through Astro 7.1.0 without route interception and
observed 202, revision 1, Material rail 88px, mobile/desktop overflow 0, and
browser diagnostics 0. Five redacted WebP assets and the A→Z procedure are rendered in
P03 complete acceptance.
P04 complaint/dispute authority remains owned by P04 and does not reopen the completed P03 account/profile scope.
Revision History
Section titled “Revision History”- 2026-07-16: accepted from the workspace owner’s standing ADR approval after the
P03 legacy and current-runtime audit. Implementation remains
not-starteduntil repository and actual actor evidence exists. - 2026-07-17: implementation moved to
partialafter the fresh no-history local actor proved registration, withdrawal, exact lost-response replay, Auth masking, profile erasure, and owner-relation cleanup. Actual browser, immutable composition, integrated six-case coverage, and history-retention cleanup remain gaps; P03 remains RED. - 2026-07-17: actual local browser and Restate network evidence closed terminal withdrawal, exact reload recovery, responsive rendering, and the seconds-unit defect. Runtime-resource cleanup, immutable composition, integrated six-case coverage, and history retention still prevent P03 PASS.
- 2026-07-17: the final actual Restate actor run, clean four-repository pins, and wrapper teardown closed persisted workflow and P03 request/Auth actor/temp/container/process cleanup. Integrated six-case coverage and history retention still prevent P03 PASS.
- 2026-07-17: the clean-pinned v2 machine actor and primary-state manifests closed the
same-composition six-case gap. The existing browser evidence remains withdrawal-only.
Historical-account operator resume, source-specific retention erasure, and retained-record
expiry/purge still keep implementation
partialand P03 RED. - 2026-07-17: backend
ed8174caadded source-owner retention/purge, review-before-effects, and atomic reviewed-withdrawal completion; contractc4b2488and frontend23d999aadded the closed admin review surface. A real unauthenticated Chromium pack proves the Material responsive/fail-closed UI, but authenticated historical actor evidence is still missing, so implementation and P03 remainpartial/RED. - 2026-07-17: clean backend
a9dd6a7, frontend864c78e, contractc4b2488, and assembly9367f44produced the historical 8-case machine pack and authenticated HQ Astro/Chromium pack. Canonical WebP, privacy, cleanup, and SSOT gates closed all P03 gaps; implementation moved toimplemented, P03 to PASS, and P04 to RED.