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
- SDK domain completion ✅ (merged)
- Auth + Supabase project + config ← this spec
- Cloud data adapter (PostgREST
StoragePort, RLS-at-runtime) - e2e harness on real auth + data
Architecture (the corrected shape)
Supabase is encapsulated in the SDK; the app never imports it.
- The
client_sdkaddspackage:supabase— the pure-Dart core (GoTrue auth + PostgREST), NOTsupabase_flutter(which is Flutter-coupled). The SDK stays pure Dart. - The
Clientfacade gains a method-extensible auth surface:currentUser/authStateChanges()/signOut, plus email/password (LIVE) (signInWithPassword/signUpWithPassword/requestPasswordReset) and the scaffolded plumbing for phone OTP (signInWithPhone→verifyPhoneOtp) and passkey (registerPasskey/signInWithPasskey). See Auth methods below. createClient(config): whenconfig.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 oneClient's auth surface — zero Supabase import in the app. - Session persistence (what
supabase_flutterwould have given us) is handled by an app-injected seam: the SDK exposes aSessionStoreinterface (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.
| Method | Status | Seam | Gate to "go live" |
|---|---|---|---|
| Email / password | LIVE | signInWithPassword / signUpWithPassword / requestPasswordReset | — |
| Phone OTP | scaffold | signInWithPhone(phone) → verifyPhoneOtp(phone, token) | an SMS provider (Twilio/MessageBird) configured in Supabase |
| Passkey / WebAuthn | scaffold | registerPasskey() / signInWithPasskey() | Dart gotrue/WebAuthn maturity + platform support + Supabase passkey enabled |
- The scaffolded methods exist on both the SDK
ClientAuthand the appAuthRepository, and throw a documentedUnimplementedError(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 emptypublic.signupstable — no conflict). Apply the app migrationsinfra/supabase/migrations/001–016in order via the Supabase MCPapply_migration, creating the household/chore/economy/goal schema + RLS alongsidesignups. - Manual prerequisite (user): toggle "Confirm email" OFF in Auth settings so
signUpreturns an instant session (we have only the publishable key, not service-role).
Config + credentials
- Creds via
--dart-define-from-file→ a gitignoredapp/config/supabase.local.json(SUPABASE_URL,SUPABASE_ANON_KEY). Matches the existingString.fromEnvironmentpattern; CI-friendly; key never committed. Add the file to.gitignore; commit asupabase.local.example.jsontemplate. - 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
SessionStoreimpl on device MUST beflutter_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:
SdkAuthRepositorydelegates correctly (a fake SDKClientauth surface). Bloc tests keep their mockedAuthRepository(unchanged). Async-throw tests useawait 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_authbuilds →signUpcreates a row inauth.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 fullcreateClientcloud 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)
- SDK Supabase auth: add
package:supabase; the GoTrue-backed auth on theClientfacade- the
SessionStoreseam;createClientwiring (auth cloud, data local); SDK unit tests.
- the
- App delegate + config:
SdkAuthRepository; thecreateAuthRepository(client)shape change;main_cloud_auth+cloudAuthConfiguration; the secureSessionStoreimpl;supabase.local.json+ .gitignore. - Project migrations: apply
001–016tobgedvvmihygwxhjxlvfu(MCP); confirm schema + RLS; email-confirm-off (user). - Smoke + verify: signUp → auth.users row (MCP) → signIn → guard → setup; the security review gate.
Risks
package:supabase(pure-Dart) session-persistence API differs fromsupabase_flutter's auto persistence — theSessionStoreseam absorbs this; confirm the exact GoTrue persistence hook early (an early plan task; use thesupabaseskill / 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).