Skip to main content

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.

TypeFunctional
Layer (build approach)Data — Supabase Auth wiring (SdkAuthRepositoryClient.auth → GoTrue)
RICER 10 × I 2 × C 50% / E 4 = 2.5 · Tier MVP-1
KPI (summary)Auth success rate; reset/email-change completion
Traces tofeature account-profile-management · C4 auth
Depends onPersonas/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_requested where 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_success on 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.

EventWhenKey propertiesFeeds
auth_successsignIn or signUp resolves without errormethod (email_password), isNewAccount (bool)KPI-A numerator; activation funnel
auth_failuresignIn or signUp throws AuthFailurereason (invalid_credentials | email_not_confirmed | rate_limited | network | unknown)KPI-A denominator; error triage
password_reset_requestedrequestPasswordReset resolves(no PII — email not logged)Reset funnel
password_reset_completedGoTrue deep-link callback confirms resetwithinTtlMinutes (coarse: leq10 | gt10)KPI-B numerator
email_changedemail-change confirmation round-trip completes(no PII)Account lifecycle
sign_outsignOut 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_type is set at the account level at creation (default free) and updated on plan change.

Scope

Account & profile management is the Supabase Auth wiring that connects the app's AuthBlocSdkAuthRepositoryClient.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 signUpWithPassword is called with {username, email, password, country?}; on success the session is persisted via SessionStore and the app navigates to onboarding.
  • Given an already-registered email When I submit Then AuthFailure is surfaced as an inline field error; no raw Supabase or GoTrue message reaches the UI (_rethrowSafe enforces 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 signInWithPassword resolves, SessionStore persists 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, SessionStore is cleared, sign_out fires, 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.

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 requestPasswordReset resolves Then a confirmation screen appears and password_reset_requested fires. 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 SessionStore When the app cold-starts Then authStateChanges() emits a non-null AuthUser without 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 SharedPreferences or 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 AuthFailure When it reaches the UI Then the message is a mapped user-facing string keyed on the reason bucket; no GoTrue error code, stack trace, or internal URL appears.
  • Rate-limited responses produce "Too many attempts — please wait a moment before trying again."
  • _rethrowSafe in SdkAuthRepository is the enforcement point: all non-AuthFailure exceptions are converted to a generic AuthFailure before 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 HouseholdMember display 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_changed fires.
  • 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.users row is created; child members are persisted as HouseholdMember rows with no linked auth.users entry.
  • Given a child member record When the SDK persists it Then no signUpWithPassword call is made and no auth.users row 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 pathAuthBloc → SdkAuthRepository → Client.auth (GoTrue). No bloc or page imports supabase directly; AuthUser and AuthFailure are the only types that cross the seam.
  • Anon (publishable) key onlyClientConfig(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 gotrueStorage hook in create_client.dart injects the persistence layer into GoTrue. The real implementation writes to flutter_secure_storage; the abstract SessionStore type (in client_config.dart) keeps the SDK free of platform I/O.
  • Children-as-profiles invariant — enforced at two points: the SDK service (no auth.signUp called for child members) and the Supabase schema (child HouseholdMember rows carry no auth.users foreign key). No UI or flow bypasses this.
  • Email change — GoTrue double-opt-in; Client.auth.updateEmail does not yet exist on the facade. FR-ACCOUNT-7 is blocked on SDK design (token handling, email_changed emission point) before it can close.
  • Last-owner-deletion and multi-house membership are open (low RICE confidence at 50%); they affect DomainRuleException guards 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; SignUpPage and ForgotPasswordPage are 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 reason bucket (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_type presentation — 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 — updateEmail design. ✅ 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_type set point. ✅ Written at sign-up (free default), so the MARKETING-bucket signal is reliable from account creation; flips to paid on successful checkout. (Not at first paywall hit — too late/unreliable.)

Out of scope (MVP-1)

  • Social login / passkeys / phone OTPregisterPasskey, signInWithPasskey, signInWithPhone, and verifyPhoneOtp are UnimplementedError scaffolds; 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 authSubscription tiers (MVP-2); plan_type is a coarse marketing signal in MVP-1 only.
  • Admin auth-event audit log — append-only audit store is deferred (Audit log).