Skip to main content

Real Supabase auth — design spec (2026-06-22)

Brainstormed + approved 2026-06-22. Sub-project 2 of 4 toward real, e2e-repeatable accounts. Replaces the InMemoryAuthRepository skeleton with real Supabase auth, fully encapsulated in the SDK — the app stays Supabase-blind.

Goal

Sign-up / sign-in against real Supabase Auth so accounts persist in auth.users and are deterministic/repeatable for e2e. Auth lives behind the client_sdk facade (the load-bearing rule: presentation never imports supabase/drift — only the facade). Data stays local until sub-project 3.

Program context

  1. SDK domain completion ✅ (merged)
  2. Auth + Supabase project + configthis spec
  3. Cloud data adapter (PostgREST StoragePort, RLS-at-runtime)
  4. e2e harness on real auth + data

Architecture (the corrected shape)

Supabase is encapsulated in the SDK; the app never imports it.

  • The client_sdk adds package:supabase — the pure-Dart core (GoTrue auth + PostgREST), NOT supabase_flutter (which is Flutter-coupled). The SDK stays pure Dart.
  • The Client facade gains a method-extensible auth surface: currentUser / authStateChanges() / signOut, plus email/password (LIVE) (signInWithPassword / signUpWithPassword / requestPasswordReset) and the scaffolded plumbing for phone OTP (signInWithPhoneverifyPhoneOtp) and passkey (registerPasskey / signInWithPasskey). See Auth methods below.
  • createClient(config): when config.api (Supabase creds) is present, auth is GoTrue-backed (real); the data adapter stays local (the PostgREST cloud adapter is sub-project 3's stub). So this build = real auth + local data.
  • The app's SdkAuthRepository implements AuthRepository (app/lib/outside/repositories/auth/) delegates to the one Client's auth surface — zero Supabase import in the app.
  • Session persistence (what supabase_flutter would have given us) is handled by an app-injected seam: the SDK exposes a SessionStore interface (read/write/clear); the app supplies a secure implementation (flutter_secure_storage → Keychain/Keystore), in-memory for dev/e2e. No Flutter coupling leaks into the SDK.

Config wiring (small shape change)

There is one Client per app session with both auth + data surfaces. Today AppConfiguration has independent createClient + createAuthRepository factories. Change createAuthRepository to receive the built client: AuthRepository Function(Client client) — the app builds the client once, then createAuthRepository(client) => SdkAuthRepository(client). dev keeps the in-memory client + InMemoryAuthRepository; a new cloud-auth config wires the Supabase-backed one.

Auth methods (extensible plumbing)

The auth surface is method-extensible so new sign-in methods slot in without reshaping the seam. Three methods are plumbed; only one is live now.

MethodStatusSeamGate to "go live"
Email / passwordLIVEsignInWithPassword / signUpWithPassword / requestPasswordReset
Phone OTPscaffoldsignInWithPhone(phone)verifyPhoneOtp(phone, token)an SMS provider (Twilio/MessageBird) configured in Supabase
Passkey / WebAuthnscaffoldregisterPasskey() / signInWithPasskey()Dart gotrue/WebAuthn maturity + platform support + Supabase passkey enabled
  • The scaffolded methods exist on both the SDK ClientAuth and the app AuthRepository, and throw a documented UnimplementedError (e.g. "phone OTP plumbing — enable when an SMS provider is configured") until wired.
  • A config carries enabled-methods flags (e.g. Set<AuthMethod> enabledMethods, default {emailPassword}) so the UI can later surface only enabled methods. The sign-in/sign-up UI keeps using email/password this sub-project — the phone/passkey UI is out of scope.
  • This keeps SP2's goal (real email/password auth, e2e-repeatable) intact while the groundwork for phone + passkey is in place.

Supabase project + migrations

  • Use the existing project bgedvvmihygwxhjxlvfu (only an empty public.signups table — no conflict). Apply the app migrations infra/supabase/migrations/001–016 in order via the Supabase MCP apply_migration, creating the household/chore/economy/goal schema + RLS alongside signups.
  • Manual prerequisite (user): toggle "Confirm email" OFF in Auth settings so signUp returns an instant session (we have only the publishable key, not service-role).

Config + credentials

  • Creds via --dart-define-from-file → a gitignored app/config/supabase.local.json (SUPABASE_URL, SUPABASE_ANON_KEY). Matches the existing String.fromEnvironment pattern; CI-friendly; key never committed. Add the file to .gitignore; commit a supabase.local.example.json template.
  • New entry app/lib/main_cloud_auth.dart + cloudAuthConfiguration: builds the Supabase-backed client (real auth) but the local data store; main_dev (in-memory auth) stays for offline UI work.

Signup attributes

At signUp, pass username + country into Supabase user_metadata (data:). They ride on the auth user (no extra table). At household setup, copy them onto the created HouseholdMember (displayName/country).

The auth↔data seam (honest)

A freshly signed-up real account has no local household → the AuthenticatedGuard routes to the Setup wizard (the real onboarding flow, now with a real account). What they create stays local until sub-project 3 persists it to Postgres per auth user.

Security (baked in)

  • Secure token store: the SessionStore impl on device MUST be flutter_secure_storage (Keychain/Keystore) — never plaintext SharedPreferences. (Mobile equivalent of "httpOnly, not localStorage".)
  • No service-role key in the app — only the publishable/anon key ships (RLS protects it).
  • Email-confirmation OFF is a scoped, temporary relaxation (dev/e2e): anyone can sign up unverified. Document it; turn confirmation back on (or add a verification gate) before real users.
  • RLS-at-runtime is still a gap — policies exist in schema but the local data path bypasses them; enforcement lands with the cloud adapter (sub-project 3). Tracked, not closed here.
  • The pasted anon key/URL are in the chat transcript — rotate if desired (safe by design).
  • Map Supabase AuthException → the app's domain error; no raw Supabase error/stack to the UI.

Testing

  • Unit: SdkAuthRepository delegates correctly (a fake SDK Client auth surface). Bloc tests keep their mocked AuthRepository (unchanged). Async-throw tests use await expectLater.
  • SDK: GoTrue-backed auth construction wired behind createClient; a fake/in-memory GoTrue for SDK unit tests (no network in tests).
  • Smoke (manual / a guarded integration test): with creds set, main_cloud_auth builds → signUp creates a row in auth.users (verify via Supabase MCP) → signIn → guard → setup.
  • e2e accounts: unique email per run (e2e+<timestamp>@rewhaven.test) — no service-role cleanup needed; the full e2e harness is sub-project 4.
  • Existing app flow tests stay on the in-memory auth (mocked repositories) — unchanged.
  • Review gates route through the ECC specialists: flutter-reviewer (Dart), database-reviewer (migrations/RLS), security-reviewer (auth/secrets).

Out of scope (later sub-projects)

  • The cloud data adapter (sub-project 3: PostgREST StoragePort, RLS-at-runtime, the full createClient cloud data path).
  • The e2e harness (sub-project 4).
  • Co-parent invite acceptance + the terms-acceptance guard (deferred per the auth audit).
  • Email confirmation ON / verification UX; password-reset email templating.
  • Phone-OTP + passkey go-live — only the seams are built here. The live phone flow (SMS provider config + the phone-entry/OTP UI) and passkey (WebAuthn platform integration + UI) are follow-ups gated on the external dependencies above.

Sequencing (for the plan)

  1. SDK Supabase auth: add package:supabase; the GoTrue-backed auth on the Client facade
    • the SessionStore seam; createClient wiring (auth cloud, data local); SDK unit tests.
  2. App delegate + config: SdkAuthRepository; the createAuthRepository(client) shape change; main_cloud_auth + cloudAuthConfiguration; the secure SessionStore impl; supabase.local.json + .gitignore.
  3. Project migrations: apply 001–016 to bgedvvmihygwxhjxlvfu (MCP); confirm schema + RLS; email-confirm-off (user).
  4. Smoke + verify: signUp → auth.users row (MCP) → signIn → guard → setup; the security review gate.

Risks

  • package:supabase (pure-Dart) session-persistence API differs from supabase_flutter's auto persistence — the SessionStore seam absorbs this; confirm the exact GoTrue persistence hook early (an early plan task; use the supabase skill / context7 if unsure).
  • The config-shape change (createAuthRepository(client)) touches the app runner — keep it minimal.
  • Email-confirm-off depends on a user dashboard toggle (flagged as a prerequisite).