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 totail/grep. client_sdkstays PURE DART —package:supabaseis pure Dart ✓;flutter_secure_storageis 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(...))— neverexpect(() => future, throwsA())(false green). - Codegen (if any): run
build_runnerfrompackages/client_sdk, scoped--build-filter, restore clobbered.g.dartsiblings. - Existing app flow tests keep the mocked in-memory auth (unchanged). dev (
main_dev) stays in-memory. - Creds NEVER committed:
app/config/supabase.local.jsonis gitignored; only a*.example.jsontemplate 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 inapp/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).InMemoryAuthRepositoryis the live impl.Clientfacade (packages/client_sdk/lib/src/client/client.dart): abstract, methods likegetHousehold(),getMembers(), etc.createClient/ClientConfig/ApiConfiginclient/.
Task 1: SDK — Supabase auth behind the facade + SessionStore seam
Files:
- Modify:
packages/client_sdk/pubspec.yaml(addsupabase) - 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
supabaseskill or context7 (mcp__plugin_context7_context7__query-docsfor "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 ofAuthState),resetPasswordForEmail(email); and the session-persistence hook (thegotrueGotrueAsyncStoragewithgetItem/setItem/removeItem, passed via the auth options, plusrecoverSession(jsonString)). Pin the exact version inpubspec.yaml. Record the confirmed signatures in the task report. (If context7 is unavailable, read the package'slib/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=$?". Confirmfvm dart pub getresolves with NO Flutter dep pulled in (it's pure Dart). - Step 3 — failing test (
supabase_auth_test.dart): driveSupabaseAuthagainst an injected fake GoTrue/SupabaseClient(no network) — assertsignInWithPassword/signUpWithPasswordcall through,authStateChanges()maps a session→AuthAccount(id,email)and sign-out→null, andSupabaseAuthExceptionmaps to a domain error. Also assert the scaffolds throw:await expectLater(auth.signInWithPhone(phone: '+1...'), throwsA(isA<UnimplementedError>()))and the same forverifyPhoneOtp/registerPasskey/signInWithPasskey. Useawait expectLater(...)for every async-throw. Run → FAIL. - Step 4 — implement
client_auth.dart(the interfaces above incl.enum AuthMethod),session_store.dart, andsupabase_auth.dart(class SupabaseAuth implements ClientAuth) wrappingSupabaseClient.auth:signInWithPassword→signInWithPassword,signUpWithPassword→signUp(..., data: metadata),requestPasswordReset→resetPasswordForEmail, mapUser→AuthAccount,onAuthStateChange→Stream<AuthAccount?>, catch SupabaseAuthException→ the SDK domain exception. The phone + passkey methods are SCAFFOLDS — each throwsUnimplementedError('phone OTP plumbing — enable when an SMS provider is configured')/UnimplementedError('passkey plumbing — enable when gotrue/WebAuthn support + platform land'). Wire theSessionStoreas the GoTrueasyncStorageadapter (read/write/clear ↔ getItem/setItem/removeItem) +recoverSessionon construction. - Step 5 — wire into
createClient: increate_client.dart/client_impl.dart, whenconfig.api != nullbuild aSupabaseClient(config.api.url, config.api.anonKey, …sessionStore…)and exposeClientImpl.auth = SupabaseAuth(supabaseClient); whenapi == nullexpose a no-op/in-memoryClientAuth. The DATA adapter stays the local/in-memory one (the cloud PostgREST data adapter is sub-project 3 — do NOT implement it here). ExportClientAuth/AuthAccount/SessionStorefrom the barrel. - Step 6 — run, expect PASS; full
client_sdksuite 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(AuthUsergainsid; newSdkAuthRepository) - Modify:
app/lib/app/configurations/configuration.dart(createAuthRepositorysignature),dev.dart,production.dart(signature),app/lib/app/builder.dart(build client once → pass tocreateAuthRepository) - Create:
app/lib/outside/repositories/auth/secure_session_store.dart(flutter_secure_storage-backedSessionStore) - 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 fakeClientwhoseauthis a controllableClientAuth; assertSdkAuthRepository.signIndelegates toclient.auth.signInWithPassword,signUpforwardsusername/countryasmetadata: {'username':…, 'country':…},authStateChanges()mapsAuthAccount→AuthUser(id,email), a thrown auth error surfaces as the domain error, and the phone/passkey methods delegate to the scaffolds (throwUnimplementedError) — all async-throws viaawait expectLater. Run → FAIL. - Step 2 —
AuthUser+AuthRepositoryseams +SdkAuthRepository: addfinal String? id;toAuthUser(keepemail;AuthUser(email: …)withidoptional). Extend theAuthRepositoryinterface with the scaffold seamssignInWithPhone({phone})/verifyPhoneOtp({phone, token})/registerPasskey()/signInWithPasskey()(so a future UI has the seam). ImplementSdkAuthRepository(this._client)delegating every method to_client.auth.*(email/pw live; phone/passkey delegate to the scaffolds that throw), mappingAuthAccount↔AuthUser,signUp→metadata. UpdateInMemoryAuthRepositoryto satisfy the extended interface — the new seam methodsthrow UnimplementedError('not supported by in-memory auth'). - Step 3 — config shape change: in
configuration.dartchangefinal AuthRepository Function() createAuthRepository;→final AuthRepository Function(Client client) createAuthRepository;. Indev.dart,createAuthRepository: (client) => InMemoryAuthRepository()(ignores the client). Inproduction.dart,createAuthRepository: (client) => throw UnimplementedError(...)(still unused). Inbuilder.dart, build the client once (final client = await config.createClient();) thenfinal 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 SessionStoreoverFlutterSecureStorage(read→read(key:),write→write(key:, value:),clear→delete(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.dartbarrel/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: addapp/config/supabase.local.jsonto.gitignore; commitapp/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): likeproduction.dartbut real auth + local data —createClient: () 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')))); }andcreateAuthRepository: (client) => SdkAuthRepository(client). (IfcreateClient'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: mirrormain_dev.dartbut build the app withcloudAuthConfiguration. Starts signed OUT (exercises anon→authenticated). - Step 4 — signUp metadata: confirm
SdkAuthRepository.signUpforwardsusername+countryintometadata(from T2) so they land inauth.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_*.sqlIN ORDER, callmcp__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 existingpublic.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(projectbgedvvmihygwxhjxlvfu, schemapublic) → expecthouseholds,household_members,chores,goals,approvals, … plussignups.mcp__plugin_supabase_supabase__get_advisors(security) → review RLS findings. - Step 3 — guarded smoke test (
supabase_auth_smoke_test.dart,skip:whenSUPABASE_URLis empty): build the cloud-auth client;signUpa uniquee2e+<timestamp>@rewhaven.test; assert a session is returned (email-confirm off) andcurrentUseris set;signInwith the same creds;signOut→currentUsernull. 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
AuthenticatedGuard→SetupRoute(the existing guard already does this; confirm it fires with the realAuthUser). - 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).