Skip to content

ADR 034 Mother Account Identity, Profile, and Consent Authority

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.

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 / 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:

  1. one verified Auth contact fact;
  2. canonical name and non-future birth date;
  3. one complete structured postal address;
  4. the exact published required policy ids, versions, and content digests; and
  5. expectedRevision plus Idempotency-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.

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 ownerSource recordBasis and durationServer-owned anchor
reservation_operationsone reservation booking and its approved contract evidenceElectronic Commerce Act contract/withdrawal record, five yearsclosed service/contract date; an open reservation is a blocker
pricing_settlementone deposited payment and approved payment/supply evidenceElectronic Commerce Act payment/supply record, five yearsdeposit or receipt-recorded date, whichever is later
pricing_settlementone finalized reservation financial lifecycle and approved ledger evidenceFramework Act on National Taxes article 85-3, five yearsexplicit legal filing deadline; missing deadline is a blocker and is never inferred from withdrawal time
care_deliveryone daily care reportElectronic Commerce Act supply evidence, or social-service provision evidence for voucher service, five yearsservice date
reservation_operationsone voucher recipient eligibility/provision recordSocial Service Use Voucher record retention, five yearsrelated service date
complaint ownerone complaint or dispute caseElectronic Commerce Act complaint/dispute record, three yearsresolved 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.

  • 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.

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.

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/422 with no primary-state mutation;
  • replay payload conflict returns 409 with no primary-state mutation;
  • registration, profile update, and withdrawal exact replays each return 200 without 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.

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.

  • 2026-07-16: accepted from the workspace owner’s standing ADR approval after the P03 legacy and current-runtime audit. Implementation remains not-started until repository and actual actor evidence exists.
  • 2026-07-17: implementation moved to partial after 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 partial and P03 RED.
  • 2026-07-17: backend ed8174ca added source-owner retention/purge, review-before-effects, and atomic reviewed-withdrawal completion; contract c4b2488 and frontend 23d999a added 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 remain partial/RED.
  • 2026-07-17: clean backend a9dd6a7, frontend 864c78e, contract c4b2488, and assembly 9367f44 produced 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 to implemented, P03 to PASS, and P04 to RED.