Skip to main content

MVP-1 Sub-project ① — Personas & Authorization: build spec

Status: approved design, ready for planning · Date: 2026-06-25 Design source of truth (read first): the rendered architecture page app/test-gallery/authored/developer/architecture/authorization.md (live at developer.eldr-labs.duckdns.org/developer/architecture/authorization). Requirements: personas-authz (authoritative) + the decisions log (D-PERSONA-1…4, D-ACCOUNT-1/2, D-COPPA-1, D-CONSENT-1…5). Review gates: flutter-reviewer (Dart), database-reviewer (PostgREST/RLS/migration), security-reviewer (zero-trust / secrets / consent gate).

Goal

Build the access + COPPA-consent substrate everything else depends on: a status-aware owner/role/kind capability matrix enforced in the SDK service and Postgres RLS (the dual gate), an append-only consent substrate (adult ToS + child VPC) with the child-PII write gate, the supervised-action path over the already-built approval policy, and the authz observability events. Full vertical: SDK + schema + RLS + app-UI. This is sub-project ① of two; ② (Money Envelopes) consumes the ②-tagged capabilities + the consentState signal.

Architecture summary

The authorization.md page is the canonical design. In brief: an authenticated adult principal acts as a household member (kind/roles/owner/status/ consentState); the pure Authorizer resolves capabilitiesFor(member) — union over kind + roles + owner, then filtered by the status + expiry + parental- kind gate; every service mutation opens with authorizer.require(cap, target); RLS mirrors each capability via SECURITY DEFINER helpers. Consent gates whether a child member's data may be persisted at all (service and schema, online and offline). Supervised action reuses the built single token-moving path.

Global Constraints (every task inherits these)

  • client_sdk stays pure Dartpackage:supabase core only; no flutter_* dep in the SDK. App-UI work lives in app/.
  • No service-role key — the app uses the anon/publishable key (sb_publishable_…) only; RLS-enforced. Any privileged op goes through an Edge Function, never a service key in the client. Reuse the existing SupabaseClient built in SP2.
  • Dual gate is mandatory — every capability and the consent gate are enforced in the SDK service AND in schema/RLS. A task that adds a service guard adds the mirrored RLS policy/trigger in the same task; RLS-parity test required.
  • Household-scoped — zero cross-household access; helpers keyed to auth.uid().
  • No child PII in analytics/authz events — opaque household-scoped IDs only; events tagged internal-BI; pass the pre-emit scrub.
  • Append-only consentsconsents rows are immutable; Member.consentState is a projection; the consent event survives child-PII erasure (opaque ref).
  • Offline parity — the consent gate holds against the local Drift store, not only the cloud.
  • Tests: async-throw uses await expectLater(..., throwsA(...)) (not expect(() => future, …)); TRUE exit codes (cmd > /tmp/t.txt 2>&1; echo EXIT=$?), never pipe test runs to tail; run build_runner scoped from packages/client_sdk and restore any clobbered .g.dart siblings; suite count ≥ baseline; verify every new column against live information_schema via the Supabase MCP (project bgedvvmihygwxhjxlvfu).
  • Anti-corruption git guardrails — no destructive git, explicit git add (never -A blindly), no *_test.dart deletions.
  • Verify built-vs-net-new with graphify first before touching code.

Net-new surface (from the built-vs-net-new gap map)

SDK (create): models/capability.dart (Capability enum), services/authorizer.dart, models/consent_state.dart, models/consent_method.dart, models/consent_record.dart, models/terms_acceptance.dart, models/member_access.dart, services/consent_service.dart. SDK (modify): models/household_member.dart (owner, consentState, consentRef; status already present), services/household_service.dart (owner/role mutations, transfer, zero-last guards, removeMember, bootstrap), services/approval_service.dart (actingMemberId supervised extension; manual/auto already built), models/exceptions.dart (new domain exceptions), the local + cloud adapters for the new columns/tables. Schema (new migrations): household_members add owner + consent_state + consent_ref (FK); consents table; terms_acceptances table; member_access (expiry); admin_household_ids()/helper_household_ids()/owner_household_ids() helpers; RLS policies per surface; guard triggers (zero-last-owner/admin, child-PII gate, status/expiry). App-UI (create): repositories + blocs + pages for role/owner management, consent lifecycle (+ adult ToS gate), supervised-action UX, and capability-aware auto_route guards. Already built (leverage, do not rebuild): MemberKind/isParental, MemberRole, MemberStatus, the ledger + zero-floor, ApprovalPolicy/ autoApproveCompletion/resolvedBy.

Phase decomposition (input to writing-plans)

Each phase ends with an independently testable deliverable; tests-first (TDD).

  1. Capability + Authorizer (pure). The capability enum + matrix + the status/expiry/parental-kind gate. Produces: Authorizer.capabilitiesFor / can / require. Tests: authorizer_test.dart — full matrix incl. helper- cannot-approve-spend/goal, non-active → empty, expired MemberAccess → empty. No I/O.
  2. Schema + RLS. Migrations: columns + consent_ref FK + consents + terms_acceptances + member_access + the three *_household_ids() helpers + RLS policies + guard triggers. Tests: RLS-parity (cloud, guarded creds flag, extends the SP3 smoke); information_schema field verification via Supabase MCP.
  3. Service guards + HouseholdService. require(...) across mutations; owner/ role mutations; transferOwnership; zero-last-owner/admin guards; removeMember; the createHousehold bootstrap grant. New exceptions. Tests: guard unit tests
    • zero-last + RLS parity.
  4. ConsentService. Child-PII write gate (online service + schema; offline Drift gate + revocation tombstone); adult TermsAcceptance + termsAcceptanceRequired; lifecycle (checkConsentCurrency → re-consent; deadline mechanism — numbers ⚖️; post-erasure retention; shadow-member ToS ordering). Tests: consent gate (granted/none/revoked), offline gate, ToS session block, currency transition.
  5. Supervised-action. Extend ApprovalService with actingMemberIdprincipalAccountId; reuse the built manual/auto single-token path; route earn by consent state. Tests: supervised over manual and auto chores; superviseAction required.
  6. Authz observability. SDK service emits authz_decision, authz_escalation_blocked, consent_state_changed, child_pii_write_rejected, supervised_action_committed — opaque IDs, internal-BI, pre-emit scrubbed. Tests: payload-scrub (no child identifier) + bucket tag.
  7. App-UI surfaces. Repositories + blocs + pages: role/owner management; consent lifecycle + adult ToS gate; supervised-action UX; capability-aware route guards + badges (authorizer.can(...), not raw isParental). Tests: flow tests per surface (mock repositories).
  8. Integration / RLS-parity + docs. Full cross-layer + RLS-parity sweep; flip authorization.md proposed → built; graphify update .. Tests: suite ≥ baseline, green analyze.

Testing strategy

Flow tests (mock repositories) for UI; integration tests (in-memory adapter, real services) for guards + consent gate online and offline; RLS-parity tests proving the schema half denies identically (the dual gate); explicit coverage for zero-last-owner/admin, helper-cannot-approve-spend/goal, status/expiry denial, and supervised over manual + auto. Suite count ≥ baseline.

Out of scope (this sub-project)

  • ② Money Envelopes — envelope generalization, moveFunds, Unallocated, goal balances, EarningsSplit wiring, the supervised-attribution household envelope. (① defines the ②-tagged capabilities + the consentState signal; ② builds the behaviors.)
  • ⚖️ counsel-confirm numbers — the 14-day/85% re-consent SLA and the VPC method details (the mechanism is in ①; the numbers/method land via counsel).
  • Self-governed mode — non-parental admin, co-parent delegated scopes, multi-household session switching.
  • TV/kiosk auth wiring (D-PERSONA-4) — the capability scope is defined; the device auth wiring is deferred.

Review routing

flutter-reviewer for Dart (SDK + app); database-reviewer for the migrations / RLS / helpers / triggers; security-reviewer for the consent gate, dual-gate completeness, no-service-role-key, and no-child-PII-in-events. Each phase routes to the relevant reviewer(s) before the next begins.