State machine · verified · newly documented

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?

Last verified 2026-04-17 against code
Slug verified-actor-hydration
Owning service FuzzyCode
Source broker.py:1202-1416

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

This state machine was not documented anywhere in 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.

flowchart TD classDef start fill:#fdf6ea,stroke:#683c06,color:#111 classDef decide fill:#fff4d6,stroke:#a47a3a,color:#111 classDef anon fill:#f1eee7,stroke:#6b6455,color:#333 classDef block fill:#fdecea,stroke:#b8432e,color:#111,font-weight:bold classDef child fill:#eaf3ff,stroke:#1d58b1,color:#111 classDef parent fill:#fdf6ea,stroke:#683c06,color:#111 classDef pact fill:#fff4d6,stroke:#e48c21,color:#111 R[Incoming request
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
Two "blocked" sub-flavors to be aware of. Session-revocation and password-rotated produce a hard 401 (the user has to log in again). The "ctx invalid" case is softer — the invalid 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

When

No JWT · JWT signature invalid · JWT expired on a protected route

Authorized for
  • Public pages (/page/<hash> public/unlisted)
  • Login / signup / OTP endpoints
Forbidden
  • Any authenticated route → 401

BLOCKED

When

Session revocation stamped · password rotated after JWT issued · child account archived/disabled

Authorized for
  • Nothing — 401 on every protected route
  • Client supervisor detects this and triggers re-login
Recovery
  • Fresh login mints new JWT with newer iat, passes cutoff

CHILD_DIRECT

When

Authenticated user is a child account with child_status=active

Principal identity

sub = child's auth user id · household = child's household

Permissions source
  • UAT: active-context token claims (per-child)
  • Prod: raw JWT claims — often lacks SUBSCRIPTION:ACTIVE
Notable

This is the state where sibling media services reject child-direct sessions in prod.

PARENT_DIRECT

When

Authenticated parent/adult user, no fc_parent_child_ctx cookie

Principal identity

sub = parent user id · household = parent's own

Authorized for
  • Parent rights endpoints (manage children, approvals)
  • Own publishing work

PARENT_ACTING_FOR_CHILD

When

Authenticated parent + valid fc_parent_child_ctx cookie + membership + active child + consent

Principal identity

sub = parent user id · household = child's · permissions scoped to child

Watch-out

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:

#CheckIf it failsAnchor
1JWT signature valid + not expired (on protected route)→ ANONYMOUSsupabase_jwt_verifier.py:36-73
2JWT iatuser's session_revoked_before→ BLOCKED (revoked)broker.py:725-736
3JWT iatuser's password_rotated_at→ BLOCKED (rotated)broker.py (issued_before_cutoff family)
4Actor row lookup from sub→ ANONYMOUS (actor missing)broker.py:1202-1416
5If child account: child_status ∈ {active, restricted} (pending/archived/deleted rejected)→ BLOCKED (child-disabled)broker.py:1100-1101 and child status migrations
6If child account: specific-child private-use consent recorded→ BLOCKEDbroker.py:1100-1101 (has_specific_child_private_use_consent)
7If adult + fc_parent_child_ctx cookie signature valid→ ignore cookie, fall through to PARENT_DIRECTbroker.py:574-606
8Parent belongs to ctx child's household + active→ ignore cookie → PARENT_DIRECTbroker.py:795-826
9Child in ctx is active + has private-use consent→ ignore cookie → PARENT_DIRECTbroker.py:1100-1101
Revocation latency gap. The cutoff comparisons at #2 and #3 depend on projections populated by 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