VerifiedActor Hydration
Given an incoming request with a JWT, what "kind of user" does FuzzyCode decide is making it — and how can that decision go wrong?
§1 Why this exists
Every protected route in FuzzyCode starts with the same question: "who is the principal making this request?" The answer isn't just a user id — the platform distinguishes parent accounts, child-direct sessions, parents acting for a child, anonymous requests, and revoked/blocked sessions. Those distinctions drive entitlement checks, PII firewall age gating, publish authorization, and parent-rights flows.
docs/ before Phase 1b. It lives entirely in broker.py:1202-1416. Every auth-related PR changes it without a visible spec. The new doc architecture adds cross-cutting/verified-actor-hydration.md as the canonical home.
§2 The state machine
A request arrives; the broker runs through a series of checks; the request ends in exactly one of five principal states. Anonymous and Blocked are terminal-for-the-request (no authorized work runs); the three active states differ in what household/child context applies.
cookies + JWT] --> J{JWT present
and verifies?} J -->|no or invalid| A[ANONYMOUS
no principal
public routes only] J -->|yes| S{Session-revocation
cutoff > JWT iat?} S -->|yes| B[BLOCKED
revoked] S -->|no| P{Password rotated
after JWT iat?} P -->|yes| B2[BLOCKED
password-rotated] P -->|no| LU[Lookup actor
from Supabase sub] LU --> HH{Is the actor
a child account?} HH -->|yes| CS{Child status
valid?} CS -->|archived / disabled| B3[BLOCKED
child-disabled] CS -->|active| CD[CHILD_DIRECT
household from child row] HH -->|no — parent/normal| PC{Has fc_parent_child_ctx
cookie + signed?} PC -->|no| PD[PARENT_DIRECT
own household] PC -->|yes| PCV{Parent is member
of ctx child's household
AND child_status=active
AND private-use consent?} PCV -->|yes| PA[PARENT_ACTING_FOR_CHILD
parent principal
but child's household/permissions] PCV -->|no| B4[BLOCKED
ctx invalid — cookie ignored,
falls back to PARENT_DIRECT] class R start class J,S,P,HH,CS,PC,PCV decide class LU start class A anon class B,B2,B3,B4 block class CD child class PD parent class PA pact
fc_parent_child_ctx cookie is ignored and the request proceeds as a plain parent. UIs that rely on the ctx cookie need to surface this fallback explicitly, otherwise a parent may silently be acting on themselves when they expected to be acting on a child.
§3 What each principal looks like at runtime
ANONYMOUS
No JWT · JWT signature invalid · JWT expired on a protected route
- Public pages (
/page/<hash>public/unlisted) - Login / signup / OTP endpoints
- Any authenticated route → 401
BLOCKED
Session revocation stamped · password rotated after JWT issued · child account archived/disabled
- Nothing — 401 on every protected route
- Client supervisor detects this and triggers re-login
- Fresh login mints new JWT with newer
iat, passes cutoff
CHILD_DIRECT
Authenticated user is a child account with child_status=active
sub = child's auth user id · household = child's household
- UAT: active-context token claims (per-child)
- Prod: raw JWT claims — often lacks
SUBSCRIPTION:ACTIVE
This is the state where sibling media services reject child-direct sessions in prod.
PARENT_DIRECT
Authenticated parent/adult user, no fc_parent_child_ctx cookie
sub = parent user id · household = parent's own
- Parent rights endpoints (manage children, approvals)
- Own publishing work
PARENT_ACTING_FOR_CHILD
Authenticated parent + valid fc_parent_child_ctx cookie + membership + active child + consent
sub = parent user id · household = child's · permissions scoped to child
Audit trails should attribute to both parent principal and acted-on child. One missing and you lose accountability.
§4 Check ordering — why it matters
The checks run in a specific order. Reordering changes semantics in subtle ways. This list anchors the intended order to code:
| # | Check | If it fails | Anchor |
|---|---|---|---|
| 1 | JWT signature valid + not expired (on protected route) | → ANONYMOUS | supabase_jwt_verifier.py:36-73 |
| 2 | JWT iat ≥ user's session_revoked_before | → BLOCKED (revoked) | broker.py:725-736 |
| 3 | JWT iat ≥ user's password_rotated_at | → BLOCKED (rotated) | broker.py (issued_before_cutoff family) |
| 4 | Actor row lookup from sub | → ANONYMOUS (actor missing) | broker.py:1202-1416 |
| 5 | If child account: child_status ∈ {active, restricted} (pending/archived/deleted rejected) | → BLOCKED (child-disabled) | broker.py:1100-1101 and child status migrations |
| 6 | If child account: specific-child private-use consent recorded | → BLOCKED | broker.py:1100-1101 (has_specific_child_private_use_consent) |
| 7 | If adult + fc_parent_child_ctx cookie signature valid | → ignore cookie, fall through to PARENT_DIRECT | broker.py:574-606 |
| 8 | Parent belongs to ctx child's household + active | → ignore cookie → PARENT_DIRECT | broker.py:795-826 |
| 9 | Child in ctx is active + has private-use consent | → ignore cookie → PARENT_DIRECT | broker.py:1100-1101 |
runtime_controls_status.py (realtime subscription + periodic reconcile). When projections lag, a blocked session may authorize for up to one reconcile interval. This is a known gap; runbook action is "run reconcile manually" in the worst case.
§5 Environment state
UAT · 2026-04-17
All five principal states function. Active-context token carries principal-scoped permissions; entitlement checks read from it. Runtime controls projections (child/parent) subscribed via Supabase realtime.
PROD · 2026-04-17
All five principal states function for broker hydration, but downstream entitlement enforcement differs: sibling media services gate on raw JWT claims, so CHILD_DIRECT without SUBSCRIPTION:ACTIVE gets 403s from Images/Sounds/Sprites even though hydration succeeded. UAT's active-context token fixes this; prod cutover is planned.
§6 Verification pointers
- Main hydration entry
FuzzyCode/quart_server/auth/broker.py:1202-1416 - Actor types
FuzzyCode/quart_server/auth/models.py - Session-revocation cutoff
broker.py:725-736(issued_before_cutoff) - Parent-child cookie verify
broker.py:574-606, 795-826 - Private-use consent check
broker.py:1100-1101 - Child status / household enums
supabase/migrations_after_baseline/20260330190000_add_household_child_foundation.sql - Household permissions builder
broker.py:904-919 - Realtime revocation projection
FuzzyCode/quart_server/services/runtime_controls_status.py:40-50 - Revocation migration
supabase/migrations_after_baseline/20260416190000_add_runtime_control_projections.sql