Skip to main content

Personas & authorization — requirements

Epic — access-control gate. MVP-1 requirements for the personas / authz goal (see the feature doc and C4 auth). Pass/fail launch conditions: an escalation defect or a child-PII write without consent fails the gate. Resolves the headline contradictions in the persona-lens gap review.

TypeNon-functional
Layer (build approach)Data — kind/role/owner model + RLS guards
RICER 10 × I 2 × C 50% / E 2 = 5.0 · Tier MVP-1
KPI (summary)Zero permission-escalation defects; access-matrix coverage % — see Success criteria
Traces tofeature personas + C4 auth
Depends onChildren's privacy (COPPA) (the consent regime this enforces) · Admin (role/kind assignment surface) · Security (RLS + crypto substrate)

Success criteria (definitive KPI)

Success = both gates hold, verified each release and sampled continuously in production:

  • Zero permission-escalation defects shipped. No actor acts outside its effective grant (a member reaching Admin, a helper moving funds, an owner-without-admin configuring, a cross-household read). One reproduced escalation fails the gate.
  • ≥95% access-matrix test coverage, every release. Every cell of the persona × action matrix (see personas) has an automated allow/deny test, measured ≥95% before ship — and zero child-PII rows exist without a valid consentRef.

Window: continuous; a single escalation or unconsented child-PII write fails the gate (a guard, not a ramped functional KPI).

Measurement & audit

Authz is provable from logs carrying no child PII — actors are opaque household-scoped ids, never minor name/age:

EventWhenKey properties
authz_decisionany guarded action resolveshouseholdRef, actorRoleSet, kind, action, allow, reasonno child PII
authz_escalation_blockeda deny fires (e.g. a child probes Admin)householdRef, action, deniedReasonno child identity
consent_state_changeda child consent-state transitionsparentRef, consentRef, from, to
child_pii_write_rejectedservice/RLS refuses a write lacking consentRefhouseholdRef, reasonno child PII
supervised_action_committedparent approves a child supervised actionparentAuthenticated, parentApprovedno child PII
(audit)release + nightlyassert 0 escalation defects, ≥95% matrix coverage, 0 child rows lacking consentRef

Three privacy buckets govern every event. Operational: child PII only after verifiable parental consent (VPC), parent-created. Internal BI: event-type + coarse props, no child identity, content-free, never marketing. Marketing: parent/account only (member count, plan type, parent-feature flags); never child-derived. We never market to a child.

Scope

The governing decision: authorization is a composition of orthogonal attributes on HouseholdMember, enforced once in the SDK service and again in schema/RLS — never in the UI alone.

  • MemberKind { parent, coParent, child } with an isParental gate (parent/coParent) — the legal-guardian / approval predicate driving approval and consent gates, not the configurable admin role.
  • MemberRole { admin, helper, member } (persisted as SQL roles text[]), layering on top of kind (kind = who, roles = what). Helper = approve/deny completed jobs only — no Admin access, no fund moves, no config.
  • owner flag — a billing-only overlay, independent of admin. Multiple owners allowed; grants Billing and nothing in config (resolves Owner⊇Admin, O1).
  • MemberStatus { shadow, invited, active } — a lifecycle overlay, not a permission tier (shadow placeholder → invited pending → active onboarded).
  • Composable overlays: a grandparent is member + helper; a parent is member + admin (+ owner).

Each child member carries a consent-state { none, pending, granted, revoked }. A child may act while a parent is the authenticated principal (supervised path). Roles + owner are per household membership, not global.

Non-functional requirements

NFR-AUTHZ-1 — Kind/role/owner/status is the single authorization model

Priority: P1 · Status: 🔨 to build Statement. Every decision derives from the member's MemberKind, MemberRole set, owner flag, and household — composed, not bespoke per feature. AcceptanceGiven a member with a role set When any guarded action is attempted Then allow/deny derives only from kind + roles + owner + household and matches the published access matrix.

NFR-AUTHZ-2 — isParental gates parental-only actions, distinct from admin

Priority: P1 · Status: 🔨 to build Statement. Approval of completions/goals/redemptions and consent acts require MemberKind.isParental; they are not grantable via the admin role, which governs configuration only. AcceptanceGiven a non-parental admin When they attempt to approve a redemption or accept child consent Then it is denied (authz_escalation_blocked), while configuration remains allowed.

NFR-AUTHZ-3 — Helper is approve/deny-jobs only, no Admin access

Priority: P1 · Status: 🔨 to build Statement. MemberRole.helper grants only completed-job approve/deny — no config, no member management, no fund moves, no goal/redemption approval. AcceptanceGiven a helper-only member When they open any Admin surface or attempt a fund move Then it is denied; When they resolve a job in the queue Then it is allowed.

NFR-AUTHZ-4 — Owner is a billing-only overlay, independent of admin

Priority: P1 · Status: 🔨 to build Statement. The owner flag grants the Billing surface and nothing else; config requires admin independently. Multiple owners allowed. Acceptance

  • Given an owner who is not an admin When they open a config surface Then it is denied (resolves gap-review O1).
  • Given a non-owner admin When they reach a paid gate Then they get an "ask an owner to upgrade" prompt, not Billing.

NFR-AUTHZ-5 — Zero-admin / zero-owner guard + transfer-ownership (per household)

Priority: P1 · Status: 🔨 to build Statement. Each household retains ≥1 admin and ≥1 owner, enforced in service AND schema. The last admin cannot self-demote; the last owner cannot delete until ownership transfers. AcceptanceGiven the last owner When they delete their account or drop the owner flag Then it is blocked until a transfer-ownership completes (same for the last admin).

NFR-AUTHZ-6 — No child PII without a valid consentRef (service + schema/RLS)

Priority: P1 · Status: 🔨 to build Statement. A child's consent-state ∈ { none, pending, granted, revoked }. No child PII is persisted unless a child-scoped consentRef is granted — enforced in the local service (offline) AND server RLS / trigger (online). Acceptance

  • Given consent-state ≠ granted When any path attempts a child-PII write Then it is rejected (child_pii_write_rejected), offline and online.
  • Given consent transitions to revoked Then further collection is refused and logged (consent_state_changed).

NFR-AUTHZ-7 — Supervised-action path: actor = child, principal = parent

Priority: P1 · Status: 🔨 to build Statement. Children are profiles under a parent account, not independent logins. Two under-13 paths: (a) supervised — child acts while a parent is the authenticated principal with per-action approval (the TV/shared-device bounty), no per-child record → consent-free; (b) own consented account — a per-child record requiring VPC (NFR-AUTHZ-6). In (a), actor ≠ authenticated principal. Acceptance

  • Given the TV with a parent authenticated and no per-child record When the child opts into a bounty Then a parent per-action approval commits it, no child PII persisted.
  • Given no authenticated parent Then the supervised action is blocked.

NFR-AUTHZ-8 — Every decision is household-scoped (RLS)

Priority: P1 · Status: 🔨 to build Statement. Roles, owner, and consent are evaluated per household membership; no actor reads or mutates another household's data. RLS is the server backstop. AcceptanceGiven an admin in household A When they request household B's data Then RLS denies it regardless of the service-layer outcome.

NFR-AUTHZ-9 — Authz logging carries no child PII; privacy buckets honored

Priority: P1 · Status: 🔨 to build Statement. Authz-decision and escalation logs reference actors by opaque household-scoped ids only. Operational child PII never enters Internal-BI or Marketing payloads; marketing telemetry is parent/account-only. AcceptanceGiven any authz decision When it is logged Then the event carries no minor identifier and no event reaches a marketing surface.

NFR-AUTHZ-10 — Standalone (untracked) Helper + status overlay

Priority: P2 · Status: 🔨 to build Statement. A helper may exist as a non-member / watch-only untracked adult (babysitter, no wallet/goals), distinct from a Member who also holds helper (grandparent). Time-boxed via the existing MemberAccess expiry; lifecycle via MemberStatus. AcceptanceGiven a babysitter invited as a standalone helper When the grant expires Then approval access ends with no tracked member record left behind.

Architecture considerations

  • Guard lives in the SDK service, not the UI. The service resolves kind + roles + owner + household into one allow/deny (repositories and blocs are thin delegates); the predicate is re-asserted in RLS / triggers so a direct DB write cannot escalate.
  • isParentaladmin. Today ApprovalService keys the approval gate on MemberKind.isParental; roles persist but don't yet drive it. This epic keeps approval/consent on isParental and scopes admin to config (gap-review A2/O1).
  • Consent invariant is a first-class guard. consentRef + consent-state join the child member; no-PII-without-consent holds offline and online and cascades on deletion. Supervised path (a) persists no per-child record, so it is outside the consent regime by construction.
  • Supervised path = "acting-as-child under a parent session + per-action approval" (actor ≠ authenticated principal) — shared with COPPA NFR-COPPA-6.
  • Household-scoped RLS isolates data; zero-admin/zero-owner and transfer-ownership are per-household invariants in service AND schema.

Design work (ahead of build)

  • Role / kind / owner assignment UI (in Admin) — assign roles, set the owner flag (multi-owner), trigger transfer-ownership, with the zero-admin / zero-owner guard surfaced as a blocked state.
  • The supervised-approval gesture — per-action parent approval on a lean-back / shared display (the TV bounty opt-in), distinct from a login.
  • Consent-state surface — the none → pending → granted → revoked lifecycle, parent-visible, wired to the pre-collection gate.
  • Access-matrix test fixtures — the persona × action grid as executable allow/deny cases feeding the ≥95% coverage KPI.

Decisions (resolved for MVP-1)

Resolved — see the MVP-1 decisions log for the canonical record, rationale, and status legend (✅ decided · ⚖️ counsel confirms · 🔜 MVP-1.x).

  • D-PERSONA-1 — Non-parental admin.No for MVP-1 — admin implies isParental; guardian/consent semantics stay coupled. Decoupling onto a separate guardian attribute is deferred to self-governed mode (A2 / X3).
  • D-PERSONA-2 — Co-parent scopes.coParent = full admin (isParental) in MVP-1; the "delegated / limited scopes" hint in context.md is deferred (A3). Reconcile context.md to note coParent = full-admin for MVP-1.
  • D-PERSONA-3 — Intra-household visibility. ✅ A child Member sees their own wallet/goals + shared Family Goals; sibling wallets/goals are not visible. RLS shaped to self + household-shared. (Set now — expensive to retrofit.)
  • D-PERSONA-4 — TV / kiosk identity. ✅ The always-on display runs under the authenticated parent's session in a read-only, privacy-reduced scope (supervised-action opt-in per the COPPA stance). The scope is defined now; the auth wiring lands with the deferred auth work.

Out of scope (MVP-1)

  • Peer / self-governed (governanceMode) mode → the self-governed modes decision (research-only; the per-household role model must not hard-code one-admin-per-house).
  • The VPC method + consent UX itselfChildren's privacy (COPPA) (this epic enforces the consent invariant; COPPA owns how consent is captured).
  • Therapist / read-only export Helper sub-variant → deferred clinical scope.