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.
| Type | Non-functional |
| Layer (build approach) | Data — kind/role/owner model + RLS guards |
| RICE | R 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 to | feature personas + C4 auth |
| Depends on | Children'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
memberreaching Admin, ahelpermoving 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:
| Event | When | Key properties |
|---|---|---|
authz_decision | any guarded action resolves | householdRef, actorRoleSet, kind, action, allow, reason — no child PII |
authz_escalation_blocked | a deny fires (e.g. a child probes Admin) | householdRef, action, deniedReason — no child identity |
consent_state_changed | a child consent-state transitions | parentRef, consentRef, from, to |
child_pii_write_rejected | service/RLS refuses a write lacking consentRef | householdRef, reason — no child PII |
supervised_action_committed | parent approves a child supervised action | parentAuthenticated, parentApproved — no child PII |
| (audit) | release + nightly | assert 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 anisParentalgate (parent/coParent) — the legal-guardian / approval predicate driving approval and consent gates, not the configurableadminrole.MemberRole { admin, helper, member }(persisted as SQLroles text[]), layering on top of kind (kind = who, roles = what). Helper = approve/deny completed jobs only — no Admin access, no fund moves, no config.ownerflag — a billing-only overlay, independent ofadmin. Multiple owners allowed; grants Billing and nothing in config (resolves Owner⊇Admin, O1).MemberStatus { shadow, invited, active }— a lifecycle overlay, not a permission tier (shadowplaceholder →invitedpending →activeonboarded).- Composable overlays: a grandparent is
member + helper; a parent ismember + 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.
Acceptance — Given 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.
Acceptance — Given 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.
Acceptance — Given 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. Acceptance — Given 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 ≠
grantedWhen any path attempts a child-PII write Then it is rejected (child_pii_write_rejected), offline and online. - Given consent transitions to
revokedThen 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. Acceptance — Given 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. Acceptance — Given 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.
Acceptance — Given 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.
isParental≠admin. TodayApprovalServicekeys the approval gate onMemberKind.isParental; roles persist but don't yet drive it. This epic keeps approval/consent onisParentaland scopesadminto 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 → revokedlifecycle, 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 —
adminimpliesisParental; 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 incontext.mdis deferred (A3). Reconcilecontext.mdto 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 itself → Children'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.