Authentication & sessions
Status: skeleton. The auth shape (bloc, guards, repository seam, RLS schema) is built and correct; the real Supabase wiring is deferred —
productionConfigurationthrowsUnimplementedError, and the only live adapter is in-memory. Treat this page as "what exists + what's undefined".
The data path
Built
AuthRepository(abstract seam) +InMemoryAuthRepository(skeleton: email/password + a broadcastauthStateChanges()stream) —app/lib/outside/repositories/auth/.AuthBloc+AuthState(status: unknown | authenticated | unauthenticated, user, errorMessage)—app/lib/inside/blocs/auth/. It reacts to the repo's auth stream (AuthUserChanged).- Guards —
AuthenticatedGuard(household == null →SetupRoute;seenIntro→ intro/welcome branch) +UnauthenticatedGuard(authenticated →HomeRoute) —app/lib/inside/routes/guards/. Welcome/Intro are intentionally unguarded. SignUpBloc(username + email + password, match validation) +ForgotPasswordBloc(callsrequestPasswordReset, currently no-op).- RLS schema —
households+household_memberspolicies viamember_household_ids()/parental_household_ids()SECURITY DEFINER functions (infra/supabase/migrations, migration 01). Invite columns (07), terms/country (08).
Gaps / under-defined
- Real Supabase auth is deferred —
productionConfigurationthrowsUnimplementedErrorforcreateClientandcreateAuthRepository; the app can't deploy against the cloud yet. - Username ↔ identity mapping unspecified — the sign-up username is captured in the UI/bloc but discarded by
InMemoryAuthRepository; noprofilestable/column or synthetic-email strategy exists. - RLS is unreachable at runtime for auth flows — the cloud data adapter (
SupabaseStorageAdapter) is now built (SP3) and does reach PostgREST under RLS, but it requires a valid Supabase JWT from the auth layer.productionConfigurationstill throwsUnimplementedErrorforcreateAuthRepository, so authenticated Supabase requests cannot be issued until the real auth wiring lands. - Co-parent invites are modelled, not wired — the
HouseholdMemberinvite fields +MemberAccessexpiry check exist, but there's no accept-invite bloc/route/deep-link, and thesend-inviteEdge Function is a stub. Invite expiry isn't RLS-enforced. - No terms-acceptance guard —
kCurrentTermsVersionis gated inHouseholdServicebut no route/page blocks an authenticated user who never accepted. AuthChangeEffect.changes()is a permanently-empty stream — live auth runs offAuthRepository.authStateChanges(); the intended migration to the effect stream is undocumented.
Admin-direct-create adult path (app-wired; cloud deploy pending)
This path lets an admin add a co-parent or other adult to the household without an email round-trip. The adult receives a one-time temp password and is forced to rotate it on first login (the "LOCKED D1/D2 credential model").
Data path
Edge Function security model (code-complete, deploy pending)
infra/supabase/functions/admin-create-adult/ is reviewed and committed but not
yet deployed to Supabase. Its gates, in order:
verify_jwt ON— the Supabase gateway rejects any call without a valid caller JWT before the handler runs.- Caller-authz dual-binding — the caller's JWT is verified server-side via
admin.auth.getUser; the caller must also be an activeparent/co_parentmember of the target household AND holdroles ⊇ ['admin']ORowner = true. A query error or missing row is fail-closed (treated as rejection). isAdultfirewall —kindmust be one ofparent | co_parent | other_adult;childor any unknown kind is rejected before any auth lookup so this path can never be a backdoor into the COPPA-gated child flow.- Credential model — generates a 192-bit URL-safe base64 temp password,
calls
createUser({ email_confirm: true, user_metadata: { must_change_password: true } }), returns the temp password once in the response (never logged). - Partial-failure rollback — if the
household_membersinsert fails after the GoTrue user was minted, the handler deletes the just-created auth user so no orphan credential lingers. A rollback failure is logged with theauth_user_idUUID (not PII, not a secret) for operator reconciliation. - Audit — appends an
invite_eventsrow withkind = 'created'(LOCKED D4); the migration for this row (invite_events'created' kind) is committed alongside the function.
Auth-seam additions (app-wired, regression-tested)
These are live in the app build:
AuthUser.mustChangePassword(bool, defaultfalse) — added to the presentation-layer auth model. Populated from GoTrueuser_metadatawhen cloud auth is wired; in-memory skeleton defaults tofalse(normal users) and clears it onupdatePassword.AuthAccount.mustChangePassword— the matching field on the SDK-layerClientAuthaccount type (packages/client_sdk/lib/src/client/client_auth.dart). Sourced from GoTrueuser_metadata.must_change_password; cleared atomically byClientAuth.updatePasswordin a single GoTrueupdateUsercall.AuthRepository.updatePassword({required String newPassword})— new abstract method on the auth seam. Delegates to GoTrueupdateUser(cloud) or clears the flag locally (in-memory skeleton). Both paths emit a newAuthUserontoauthStateChanges()so the guard can react without a page reload.
MustChangePasswordGuard (app-wired, un-bypassable, regression-tested)
app/lib/inside/routes/guards/must_change_password_guard.dart — an
AutoRouteGuard that runs on every authenticated route after AuthenticatedGuard:
- If
AuthRepository.currentUser?.mustChangePassword == trueand the target route is notSetPasswordRoute, the guard pushesSetPasswordRouteand aborts the original navigation. SetPasswordRouteis always allowed through (loop-prevention).- A
nulluser ormustChangePassword == falsepasses immediately (normal users are never trapped; signed-out users are caught byAuthenticatedGuardfirst). - Wired in
router.dart; covered bymust_change_password_guard_test.dartand the router gate coverage test (router_gate_coverage_test.dart).
End-to-end completeness: the app-side wiring (guard + seam + admin UI) is
complete and tested. The full D1/D2 credential flow (admin creates adult → app
blocks until password rotated) becomes end-to-end functional once the
admin-create-adult Edge Function is deployed to the Supabase project.
See the auth/onboarding branding spec for the onboarding flow and the hosting & auth decision.