Account & profile management — requirements
Epic. MVP-1 requirements breakdown for the Account & profile management goal (see the feature architecture). Each FR is sized to become one or a few user stories. The auth screens (
SignUpPage,ForgotPasswordPage) are partially scaffolded; real GoTrue wiring, the session store, and the account-holder profile-edit surface close the gap.
| Type | Functional |
| Layer (build approach) | Data — Supabase Auth wiring (SdkAuthRepository → Client.auth → GoTrue) |
| RICE | R 10 × I 2 × C 50% / E 4 = 2.5 · Tier MVP-1 |
| KPI (summary) | Auth success rate; reset/email-change completion |
| Traces to | feature account-profile-management · C4 auth |
| Depends on | Personas/authz · Security & data protection · Children's privacy (COPPA) |
Success criteria (definitive KPI)
Success = auth success rate ≥ 97%, password-reset completion ≥ 65% within 10 minutes, AND < 2% of sessions record an auth error — all three gates held across the first 4 weeks post-launch.
- Metric A (auth success rate):
auth_success/ (auth_success+auth_failure) per calendar week. Target: ≥ 97%. A degradation below 95% in any week is a P1 incident. - Metric B (reset completion):
password_reset_completed/password_reset_requestedwhere the reset lands within 10 minutes of the request. Target: ≥ 65%. The 10-minute window matches the GoTrue reset-token TTL; events outside it are tracked but excluded from the numerator. - Metric C (session error rate): sessions that emit ≥ 1
auth_failure/ total sessions. Target: < 2%. - Window: first 4 weeks post-launch (activation = first successful
auth_successon the account). - Baseline: no install base yet — treat 97% / 65% / 2% as the launch hypothesis. Re-baseline from the first cohort's week-2 data before adjusting.
- Why these bars: 97% success rate and < 2% error rate are trust-floor targets; reset-completion is the deliverability and UX-clarity canary.
Analytics — events to record
All events are INTERNAL BI — content-free, no child identity, never marketing
(see COPPA and the privacy model note
below). plan_type is a MARKETING-bucket signal emitted at the account level
(parent only); it is never derived from child data and never used to target children.
| Event | When | Key properties | Feeds |
|---|---|---|---|
auth_success | signIn or signUp resolves without error | method (email_password), isNewAccount (bool) | KPI-A numerator; activation funnel |
auth_failure | signIn or signUp throws AuthFailure | reason (invalid_credentials | email_not_confirmed | rate_limited | network | unknown) | KPI-A denominator; error triage |
password_reset_requested | requestPasswordReset resolves | (no PII — email not logged) | Reset funnel |
password_reset_completed | GoTrue deep-link callback confirms reset | withinTtlMinutes (coarse: leq10 | gt10) | KPI-B numerator |
email_changed | email-change confirmation round-trip completes | (no PII) | Account lifecycle |
sign_out | signOut resolves | (no properties) | Session lifecycle |
Privacy model. OPERATIONAL: child PII stays inside the VPC, never emitted. INTERNAL BI: the rows above — content-free, no child identity, never marketing. MARKETING: parent/account only — aggregate count,
plan_type, parent-feature-active flags; never child-derived.plan_typeis set at the account level at creation (defaultfree) and updated on plan change.
Scope
Account & profile management is the Supabase Auth wiring that connects the app's
AuthBloc → SdkAuthRepository → Client.auth (GoTrue) to a live Supabase project.
It covers: the adult account holder's full auth lifecycle (sign-up, sign-in, sign-out,
password reset, email change), secure session persistence via the SessionStore /
GoTrue storage hook, and the account holder's own profile edit (display name, email).
The parent/adult is the account holder. Children are member profiles added by the
parent — they have no Supabase Auth identity, no credentials, and no login screen.
Under-13s act under the parent's active session or via the supervised TV path, consistent
with prior granted VPC (see COPPA). plan_type
is an attribute of the adult account, not of any child profile.
Functional requirements
FR-ACCOUNT-1 — Adult-only sign-up
Priority: P1 · Status: 🔨 partial — SignUpPage scaffolded; real GoTrue call wired but onboarding nav + error-state UI incomplete
Statement. As a new user, I can create an adult Rewhaven account with an email and
password so I become the account holder who controls the household.
Acceptance
- Given valid email + password (≥ 8 chars) When I submit Then
signUpWithPasswordis called with{username, email, password, country?}; on success the session is persisted viaSessionStoreand the app navigates to onboarding. - Given an already-registered email When I submit Then
AuthFailureis surfaced as an inline field error; no raw Supabase or GoTrue message reaches the UI (_rethrowSafeenforces this). - The sign-up form creates an adult account only; no path exists to register a child directly.
FR-ACCOUNT-2 — Sign-in with email/password
Priority: P1 · Status: 🔨 partial — signIn on SdkAuthRepository implemented; screen error-state and routing incomplete
Statement. As a returning account holder, I can sign in with my email and password so
I reach my household.
Acceptance
- Given correct credentials When I sign in Then
signInWithPasswordresolves,SessionStorepersists the session,auth_success(isNewAccount: false)fires, and I land on the home tab. - Given wrong credentials Then
AuthFailure→ inline error;auth_failure(reason: invalid_credentials)fires. No GoTrue error code or message is shown. - Given network failure Then
auth_failure(reason: network)fires; a retry prompt appears.
FR-ACCOUNT-3 — Sign-out
Priority: P1 · Status: 🔨 to build — signOut on SdkAuthRepository exists; the Profile-page sign-out entry point (bottom of the page) is the surface
Statement. As an account holder, I can sign out so my session is cleared on this device.
Acceptance
- Given an active session When I sign out Then
Client.auth.signOut()is called,SessionStoreis cleared,sign_outfires, and the app navigates to the sign-in screen. - Given sign-out while offline Then the local session is cleared immediately (GoTrue local invalidation); remote revocation is attempted on next connectivity.
FR-ACCOUNT-4 — Forgot-password (request + deep-link reset)
Priority: P1 · Status: 🔨 partial — ForgotPasswordPage scaffolded; requestPasswordReset wired; deep-link handler and new-password screen missing
Statement. As an account holder who has forgotten their password, I can request a reset
link by email and complete the reset via the GoTrue deep link.
Acceptance
- Given I submit any email When
requestPasswordResetresolves Then a confirmation screen appears andpassword_reset_requestedfires. The response is the same whether the email is registered or not (no enumeration). - Given I tap the reset link within 10 minutes When the deep-link handler resolves the GoTrue token Then I land on a new-password screen; on submit,
password_reset_completed(withinTtlMinutes: leq10)fires. - Given an expired token (> 10 min) Then a clear "link expired — request a new one" screen is shown;
password_reset_completed(withinTtlMinutes: gt10)fires for funnel visibility.
FR-ACCOUNT-5 — Secure session persistence (SessionStore)
Priority: P1 · Status: 🔨 to build — gotrueStorage hook wired in create_client.dart; real secure-storage implementation pending
Statement. As a returning user, my session persists across app restarts so I do not
re-authenticate on every launch.
Acceptance
- Given a valid session in
SessionStoreWhen the app cold-starts ThenauthStateChanges()emits a non-nullAuthUserwithout a sign-in prompt. - Given the stored token is expired When the app cold-starts Then GoTrue attempts a silent refresh; on failure the user is routed to sign-in without a crash.
- Session tokens are stored in platform-secure storage (Keychain on iOS; EncryptedSharedPreferences on Android) — never in
SharedPreferencesor plaintext files.
FR-ACCOUNT-6 — Auth-error feedback without internal leakage
Priority: P1 · Status: 🔨 to build — _rethrowSafe guards the SDK seam; UI message-mapping and copy missing
Statement. As a user who encounters an auth error, I see a clear, actionable message
with no raw Supabase or GoTrue text exposed.
Acceptance
- Given any
AuthFailureWhen it reaches the UI Then the message is a mapped user-facing string keyed on thereasonbucket; no GoTrue error code, stack trace, or internal URL appears. - Rate-limited responses produce "Too many attempts — please wait a moment before trying again."
_rethrowSafeinSdkAuthRepositoryis the enforcement point: all non-AuthFailureexceptions are converted to a genericAuthFailurebefore crossing the seam.
FR-ACCOUNT-7 — Account-holder profile edit (display name + email change)
Priority: P2 · Status: 🔨 to build — updateEmail facade method does not yet exist on Client.auth; SDK work required
Statement. As an account holder, I can update my display name and request an email
address change so my account details stay current.
Acceptance
- Given I submit a new display name When the update resolves Then the profile header reflects the change; the parent's
HouseholdMemberdisplay name is updated via the SDK. - Given I submit a new email When GoTrue sends confirmation to both old and new addresses Then the UI shows "Check both inboxes to confirm the change"; on round-trip confirmation,
email_changedfires. - Email change follows GoTrue's double-opt-in flow; the old email remains active until confirmed on both sides.
FR-ACCOUNT-8 — Children are profiles, not logins
Priority: P1 · Status: 🔨 to build — architectural invariant; enforced in SDK service + UI; no separate implementation surface
Statement. As the app, I ensure that children added by a parent are HouseholdMember
profiles only — they have no Supabase Auth identity and no login screen.
Acceptance
- Given any sign-up or sign-in flow When it completes Then only an adult
auth.usersrow is created; child members are persisted asHouseholdMemberrows with no linkedauth.usersentry. - Given a child member record When the SDK persists it Then no
signUpWithPasswordcall is made and noauth.usersrow is created or referenced for that child. - The TV supervised-bounty path is the only way a child acts without the parent's foreground session, and it requires the parent's active session + per-action approval (see COPPA NFR-COPPA-6).
Architecture considerations
- One data path —
AuthBloc → SdkAuthRepository → Client.auth(GoTrue). No bloc or page importssupabasedirectly;AuthUserandAuthFailureare the only types that cross the seam. - Anon (publishable) key only —
ClientConfig(api: ApiConfig(url:, anonKey:))is the only credential in the app binary. No service-role key ships to the client; any privileged auth operation uses a Supabase Edge Function. - RLS activation — every post-auth request carries the user's JWT; Supabase RLS policies scope all reads/writes to the account holder's household. Wiring GoTrue does not change RLS policies — it activates them.
- SessionStore — the
gotrueStoragehook increate_client.dartinjects the persistence layer into GoTrue. The real implementation writes toflutter_secure_storage; the abstractSessionStoretype (inclient_config.dart) keeps the SDK free of platform I/O. - Children-as-profiles invariant — enforced at two points: the SDK service (no
auth.signUpcalled for child members) and the Supabase schema (childHouseholdMemberrows carry noauth.usersforeign key). No UI or flow bypasses this. - Email change — GoTrue double-opt-in;
Client.auth.updateEmaildoes not yet exist on the facade. FR-ACCOUNT-7 is blocked on SDK design (token handling,email_changedemission point) before it can close. - Last-owner-deletion and multi-house membership are open (low RICE confidence at 50%); they affect
DomainRuleExceptionguards and RLS tenant binding but not the MVP-1 happy path.
Design work (ahead of build)
- Reset deep-link landing screen — new-password entry after clicking the GoTrue reset email;
SignUpPageandForgotPasswordPageare scaffolded but this screen is missing. Needs error-state treatment for expired tokens. - Sign-in error states — inline field error vs snackbar decision; copy for each
reasonbucket (invalid credentials, rate-limited, network). - Account-holder profile edit surface — a settings card reachable from the Profile hub; must make the parent/account-holder context visually distinct from child member profiles. The "add a child" flow should show the child being added as a profile, never as registering.
plan_typepresentation — where the account tier is shown in Settings (read-only in MVP-1; upgrade path is MVP-2).
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-ACCOUNT-1 — Last-owner deletion. ✅ Block deletion while the account is the sole owner of a household with other members — require ownership transfer first, or an explicit "delete household + all data" confirm. Cascade-delete only when the owner is the last member. Guard:
LastOwnerCannotDeleteWithMembers. (This is the same primitive a GDPR/CCPA erasure request reuses.) - D-ACCOUNT-2 — Multi-household membership. ✅ The data model stays forward-compatible (membership is a join), but MVP-1 binds one active household per session. Multi-household session-switching is deferred (X-tier); RLS keeps the household-id binding single-active.
- D-ACCOUNT-3 — Social login. 🔜 Defer to MVP-2. Email/password only in MVP-1 — explicit gate before MVP-2.
- D-ACCOUNT-4 —
updateEmaildesign. ✅ MVP-1 uses Supabase Auth's built-in email-change flow (double-opt-in, confirm on both addresses). SDK:client.updateEmail(newEmail)returns a pending-confirmation state. FR-ACCOUNT-7 schedulable on that signature. - D-ACCOUNT-5 —
plan_typeset point. ✅ Written at sign-up (freedefault), so the MARKETING-bucket signal is reliable from account creation; flips topaidon successful checkout. (Not at first paywall hit — too late/unreliable.)
Out of scope (MVP-1)
- Social login / passkeys / phone OTP —
registerPasskey,signInWithPasskey,signInWithPhone, andverifyPhoneOtpareUnimplementedErrorscaffolds; all deferred. - Multi-house membership + session-switching — data model is forward-compatible; UI and session-context switching are later.
- Account deletion with cascade — the deletion trigger, ledger handling, and GDPR erasure path live in Privacy & GDPR (required pre-EU launch, not US MVP-1).
- Subscription / paywall gating on auth — Subscription tiers (MVP-2);
plan_typeis a coarse marketing signal in MVP-1 only. - Admin auth-event audit log — append-only audit store is deferred (Audit log).