Skip to main content

Real Supabase Auth Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans. Steps use checkbox (- [ ]) syntax. Review gates route through the ECC specialists: flutter-reviewer (Dart), database-reviewer (migrations/RLS), security-reviewer (auth/secrets).

Goal: Replace the in-memory auth skeleton with real Supabase auth, encapsulated in the SDK — so accounts persist in auth.users and are e2e-repeatable. The app stays Supabase-blind.

Architecture: client_sdk adds package:supabase (pure-Dart GoTrue) and exposes a ClientAuth surface on the Client facade; the cloud path of createClient wires GoTrue auth while data stays local (cloud data = sub-project 3). The app's SdkAuthRepository delegates to client.auth; session persistence is an app-injected SessionStore seam (secure storage). Sub-project 2 of 4.

Tech Stack: Dart 3.9, package:supabase (pure-Dart, SDK), flutter_secure_storage (app only), FVM. NO supabase_flutter, NO Node.

Global Constraints

  • FVM: fvm dart … / fvm flutter …. TRUE exit codes: <cmd> > /tmp/t.txt 2>&1; echo "EXIT=$?"; inspect the file. Never pipe a test/codegen run to tail/grep.
  • client_sdk stays PURE DARTpackage:supabase is pure Dart ✓; flutter_secure_storage is an app-only dep (the SessionStore impl lives in the app, the interface in the SDK).
  • No supabase_flutter (Flutter-coupled). No service-role key in the app — publishable/anon key only.
  • Map Supabase AuthException → the app domain error (DomainRuleException/the auth bloc's error) — no raw Supabase message/stack to the UI.
  • Async-throw tests use await expectLater(future, throwsA(...)) — never expect(() => future, throwsA()) (false green).
  • Codegen (if any): run build_runner from packages/client_sdk, scoped --build-filter, restore clobbered .g.dart siblings.
  • Existing app flow tests keep the mocked in-memory auth (unchanged). dev (main_dev) stays in-memory.
  • Creds NEVER committed: app/config/supabase.local.json is gitignored; only a *.example.json template is committed.
  • Email-confirmation OFF is a user dashboard prerequisite (we have only the publishable key) — flag it, don't assume it.
  • Commit per task on feat/supabase-auth; do NOT push.

Verified anchors

  • AppConfiguration (app/lib/app/configurations/configuration.dart): final Future<Client> Function() createClient; + final AuthRepository Function() createAuthRepository;. Consumed in app/lib/app/builder.dart. Configs: dev.dart (in-memory), production.dart (throws).
  • AuthRepository (app/lib/outside/repositories/auth/auth_repository.dart): currentUser → AuthUser?, authStateChanges() → Stream<AuthUser?>, signIn({email,password}), signUp({username,email,password,country?}), requestPasswordReset({email}), signOut(). AuthUser{required String email} (today). InMemoryAuthRepository is the live impl.
  • Client facade (packages/client_sdk/lib/src/client/client.dart): abstract, methods like getHousehold(), getMembers(), etc. createClient/ClientConfig/ApiConfig in client/.

Task 1: SDK — Supabase auth behind the facade + SessionStore seam

Files:

  • Modify: packages/client_sdk/pubspec.yaml (add supabase)
  • Create: packages/client_sdk/lib/src/client/client_auth.dart (ClientAuth + AuthAccount), packages/client_sdk/lib/src/client/session_store.dart (SessionStore), packages/client_sdk/lib/src/adapters/cloud/supabase_auth.dart
  • Modify: packages/client_sdk/lib/src/client/client.dart (ClientAuth get auth), client/client_impl.dart, client/client_config.dart (+ SessionStore? sessionStore), client/create_client.dart, the package barrel
  • Test: packages/client_sdk/test/client/supabase_auth_test.dart

Interfaces — Produces:

class AuthAccount extends Equatable { // SDK-side authed user
const AuthAccount({required this.id, required this.email});
final String id; // auth.users uuid
final String email;
@override List<Object?> get props => [id, email];
}
enum AuthMethod { emailPassword, phoneOtp, passkey }
abstract class ClientAuth {
AuthAccount? get currentUser;
Stream<AuthAccount?> authStateChanges();
Future<void> signOut();
// email/password — LIVE
Future<void> signInWithPassword({required String email, required String password});
Future<void> signUpWithPassword({required String email, required String password, Map<String, dynamic>? metadata});
Future<void> requestPasswordReset({required String email});
// phone OTP — SCAFFOLD (throws UnimplementedError until an SMS provider is configured)
Future<void> signInWithPhone({required String phone});
Future<void> verifyPhoneOtp({required String phone, required String token});
// passkey/WebAuthn — SCAFFOLD (throws UnimplementedError until gotrue/WebAuthn + platform support land)
Future<void> registerPasskey();
Future<void> signInWithPasskey();
}
abstract class SessionStore { // app-injected persistence seam — MULTI-KEY (GoTrue contract)
Future<String?> read(String key);
Future<void> write(String key, String value);
Future<void> clear(String key); // remove a single key (NOT the whole store)
}

Client gains ClientAuth get auth;. ClientConfig gains final SessionStore? sessionStore; + final Set<AuthMethod> enabledMethods; (default const {AuthMethod.emailPassword}).

  • Step 1 — CONFIRM the package:supabase auth API FIRST. Use the supabase skill or context7 (mcp__plugin_context7_context7__query-docs for "supabase" Dart) to confirm, for the pinned version: SupabaseClient(url, anonKey) construction; client.auth.signInWithPassword(email:, password:), signUp(email:, password:, data:), signOut(), currentUser/currentSession, onAuthStateChange (Stream of AuthState), resetPasswordForEmail(email); and the session-persistence hook (the gotrue GotrueAsyncStorage with getItem/setItem/removeItem, passed via the auth options, plus recoverSession(jsonString)). Pin the exact version in pubspec.yaml. Record the confirmed signatures in the task report. (If context7 is unavailable, read the package's lib/ from the pub cache.)
  • Step 2 — add the dep: cd packages/client_sdk && fvm dart pub add supabase:<pinned version> > /tmp/t.txt 2>&1; echo "EXIT=$?". Confirm fvm dart pub get resolves with NO Flutter dep pulled in (it's pure Dart).
  • Step 3 — failing test (supabase_auth_test.dart): drive SupabaseAuth against an injected fake GoTrue/SupabaseClient (no network) — assert signInWithPassword/signUpWithPassword call through, authStateChanges() maps a session→AuthAccount(id,email) and sign-out→null, and SupabaseAuthException maps to a domain error. Also assert the scaffolds throw: await expectLater(auth.signInWithPhone(phone: '+1...'), throwsA(isA<UnimplementedError>())) and the same for verifyPhoneOtp/registerPasskey/signInWithPasskey. Use await expectLater(...) for every async-throw. Run → FAIL.
  • Step 4 — implement client_auth.dart (the interfaces above incl. enum AuthMethod), session_store.dart, and supabase_auth.dart (class SupabaseAuth implements ClientAuth) wrapping SupabaseClient.auth: signInWithPasswordsignInWithPassword, signUpWithPasswordsignUp(..., data: metadata), requestPasswordResetresetPasswordForEmail, map UserAuthAccount, onAuthStateChangeStream<AuthAccount?>, catch Supabase AuthException → the SDK domain exception. The phone + passkey methods are SCAFFOLDS — each throws UnimplementedError('phone OTP plumbing — enable when an SMS provider is configured') / UnimplementedError('passkey plumbing — enable when gotrue/WebAuthn support + platform land'). Wire the SessionStore as the GoTrue asyncStorage adapter (read/write/clear ↔ getItem/setItem/removeItem) + recoverSession on construction.
  • Step 5 — wire into createClient: in create_client.dart/client_impl.dart, when config.api != null build a SupabaseClient(config.api.url, config.api.anonKey, …sessionStore…) and expose ClientImpl.auth = SupabaseAuth(supabaseClient); when api == null expose a no-op/in-memory ClientAuth. The DATA adapter stays the local/in-memory one (the cloud PostgREST data adapter is sub-project 3 — do NOT implement it here). Export ClientAuth/AuthAccount/SessionStore from the barrel.
  • Step 6 — run, expect PASS; full client_sdk suite green: cd packages/client_sdk && fvm dart test > /tmp/t.txt 2>&1; echo "EXIT=$?".
  • Step 7 — commit: git add packages/client_sdk && git commit -m "feat(sdk): Supabase auth (GoTrue) on the Client facade + SessionStore seam (SP2 T1)"

Task 2: App — SdkAuthRepository + the createAuthRepository(client) shape change

Files:

  • Modify: app/lib/outside/repositories/auth/auth_repository.dart (AuthUser gains id; new SdkAuthRepository)
  • Modify: app/lib/app/configurations/configuration.dart (createAuthRepository signature), dev.dart, production.dart (signature), app/lib/app/builder.dart (build client once → pass to createAuthRepository)
  • Create: app/lib/outside/repositories/auth/secure_session_store.dart (flutter_secure_storage-backed SessionStore)
  • Modify: app/pubspec.yaml (flutter_secure_storage)
  • Test: app/test/unit/sdk_auth_repository_test.dart

Interfaces — Consumes: Task 1's Client.auth (ClientAuth), AuthAccount, SessionStore. Produces: AuthUser{id, email}; SdkAuthRepository(Client client) implements AuthRepository; AppConfiguration.createAuthRepository is now AuthRepository Function(Client client); SecureSessionStore implements SessionStore.

  • Step 1 — failing test (sdk_auth_repository_test.dart): a fake Client whose auth is a controllable ClientAuth; assert SdkAuthRepository.signIn delegates to client.auth.signInWithPassword, signUp forwards username/country as metadata: {'username':…, 'country':…}, authStateChanges() maps AuthAccountAuthUser(id,email), a thrown auth error surfaces as the domain error, and the phone/passkey methods delegate to the scaffolds (throw UnimplementedError) — all async-throws via await expectLater. Run → FAIL.
  • Step 2 — AuthUser + AuthRepository seams + SdkAuthRepository: add final String? id; to AuthUser (keep email; AuthUser(email: …) with id optional). Extend the AuthRepository interface with the scaffold seams signInWithPhone({phone}) / verifyPhoneOtp({phone, token}) / registerPasskey() / signInWithPasskey() (so a future UI has the seam). Implement SdkAuthRepository(this._client) delegating every method to _client.auth.* (email/pw live; phone/passkey delegate to the scaffolds that throw), mapping AuthAccountAuthUser, signUpmetadata. Update InMemoryAuthRepository to satisfy the extended interface — the new seam methods throw UnimplementedError('not supported by in-memory auth').
  • Step 3 — config shape change: in configuration.dart change final AuthRepository Function() createAuthRepository;final AuthRepository Function(Client client) createAuthRepository;. In dev.dart, createAuthRepository: (client) => InMemoryAuthRepository() (ignores the client). In production.dart, createAuthRepository: (client) => throw UnimplementedError(...) (still unused). In builder.dart, build the client once (final client = await config.createClient();) then final authRepository = config.createAuthRepository(client); — wire both into the providers. Run the app analyzer: cd app && fvm flutter analyze > /tmp/a.txt 2>&1; echo "EXIT=$?" → clean.
  • Step 4 — secure SessionStore: fvm flutter pub add flutter_secure_storage (app); SecureSessionStore implements SessionStore over FlutterSecureStorage (readread(key:), writewrite(key:, value:), cleardelete(key:), single key e.g. 'rewhaven.session').
  • Step 5 — run app + sdk suites green (cd app && fvm flutter test; cd packages/client_sdk && fvm dart test), TRUE exit codes. Commit: feat(app): SdkAuthRepository + createAuthRepository(client) + secure SessionStore (SP2 T2)

Task 3: cloudAuthConfiguration + main_cloud_auth entry + creds

Files:

  • Create: app/lib/app/configurations/cloud_auth.dart (cloudAuthConfiguration), app/lib/main_cloud_auth.dart, app/config/supabase.local.example.json
  • Modify: app/lib/app/configurations/configuration.dart barrel/exports if needed; .gitignore (app/config/supabase.local.json)

Interfaces — Consumes: T1 (createClient cloud path, SessionStore), T2 (SdkAuthRepository, SecureSessionStore, the new createAuthRepository(client) signature).

  • Step 1 — .gitignore + creds template: add app/config/supabase.local.json to .gitignore; commit app/config/supabase.local.example.json:
{ "SUPABASE_URL": "https://YOUR-REF.supabase.co", "SUPABASE_ANON_KEY": "sb_publishable_..." }

(The real supabase.local.json with the project creds is created locally, NOT committed.)

  • Step 2 — cloudAuthConfiguration (cloud_auth.dart): like production.dart but real auth + local datacreateClient: () async { final dir = (await getApplicationDocumentsDirectory()).path; return createClient(config: ClientConfig(localStorageDirectory: dir, sessionStore: SecureSessionStore(), api: ApiConfig(url: Uri.parse(const String.fromEnvironment('SUPABASE_URL')), anonKey: const String.fromEnvironment('SUPABASE_ANON_KEY')))); } and createAuthRepository: (client) => SdkAuthRepository(client). (If createClient's cloud data path is still a stub, ensure it falls back to local Drift/in-memory data — auth is the only cloud surface this sub-project lights up.)
  • Step 3 — main_cloud_auth.dart: mirror main_dev.dart but build the app with cloudAuthConfiguration. Starts signed OUT (exercises anon→authenticated).
  • Step 4 — signUp metadata: confirm SdkAuthRepository.signUp forwards username+country into metadata (from T2) so they land in auth.users.user_metadata.
  • Step 5 — analyze + build the entry:
cd app && fvm flutter analyze > /tmp/a.txt 2>&1; echo "ANALYZE=$?"
fvm flutter build web --release -t lib/main_cloud_auth.dart --dart-define-from-file=config/supabase.local.json > /tmp/b.txt 2>&1; echo "BUILD=$?"

(Requires a local app/config/supabase.local.json with the real creds — the executor creates it, uncommitted.) Expected ANALYZE=0, BUILD=0.

  • Step 6 — commit: feat(app): cloudAuthConfiguration + main_cloud_auth + gitignored creds (SP2 T3)

Task 4: Apply migrations to the project + smoke-verify real auth

Files: none (uses the Supabase MCP + a guarded smoke test) — Create: app/test/integration/supabase_auth_smoke_test.dart (skipped unless creds present)

Prerequisite (USER): in the Supabase dashboard for project bgedvvmihygwxhjxlvfu → Authentication → Providers/Settings → turn OFF "Confirm email". (Controller cannot do this with the publishable key.)

  • Step 1 — apply migrations (Supabase MCP, controller-run): for each file infra/supabase/migrations/20260612000001…016_*.sql IN ORDER, call mcp__plugin_supabase_supabase__apply_migration (name = the file's base name, query = its SQL). These create the household/chore/economy/goal schema + RLS alongside the existing public.signups (no conflict). This touches the live project — run deliberately, one at a time, stopping on any error.
  • Step 2 — confirm schema: mcp__plugin_supabase_supabase__list_tables (project bgedvvmihygwxhjxlvfu, schema public) → expect households, household_members, chores, goals, approvals, … plus signups. mcp__plugin_supabase_supabase__get_advisors (security) → review RLS findings.
  • Step 3 — guarded smoke test (supabase_auth_smoke_test.dart, skip: when SUPABASE_URL is empty): build the cloud-auth client; signUp a unique e2e+<timestamp>@rewhaven.test; assert a session is returned (email-confirm off) and currentUser is set; signIn with the same creds; signOutcurrentUser null. Run with the creds dart-define. Then verify the user exists: mcp__plugin_supabase_supabase__execute_sql (select count(*) from auth.users where email like 'e2e+%@rewhaven.test') > 0.
  • Step 4 — guard routing check: a widget/integration assertion (or manual note) that a freshly-authenticated no-household account hits AuthenticatedGuardSetupRoute (the existing guard already does this; confirm it fires with the real AuthUser).
  • Step 5 — commit: test(app): supabase auth smoke + migrations applied to project (SP2 T4) (the smoke test only; migrations live in the cloud project, not the repo).

Self-review

  • Spec coverage: SDK Supabase auth on the facade + SessionStore (T1); SdkAuthRepository + createAuthRepository(client) shape change + secure store (T2); cloudAuthConfiguration + main_cloud_auth + gitignored creds + user_metadata (T3); migrations applied + smoke/guard verify + email-confirm-off prerequisite (T4). Security (secure store, no service-role, AuthException-mapped, RLS-at-runtime noted as SP3 gap) woven across T1/T2/T3. Auth-cloud/data-local split explicit in T1.S5 + T3.S2. ✓
  • Placeholders: the package:supabase API specifics are gated behind T1.S1 (confirm via context7/supabase skill + pin version) — the one honest unknown, made an explicit first step, not a silent TODO. Interfaces (ClientAuth/AuthAccount/SessionStore/SdkAuthRepository) are concrete. ✓
  • Type consistency: AuthAccount{id,email}, ClientAuth, SessionStore, AuthUser{id?,email}, SdkAuthRepository(Client), createAuthRepository(Client) used consistently across T1→T4. ✓
  • Risk: the migration application + the email-confirm toggle touch the live project — both flagged as deliberate/gated (controller-run MCP / user dashboard).