Authentication

Email auth, OTP callbacks, Google Sign-In, sessions, and account actions.

The template wraps Supabase Auth with a small repository layer, a SessionController, and Compose screens for email auth, OTP verification, password recovery, Google Sign-In, and account security actions.

What it provides

SurfaceStatusImplementation
Email/password sign-inIntegratedSupabaseAuthRemoteDataSource.signIn() with the Supabase Email provider
Sign-up + OTP verificationIntegratedsignUp(), verifyEmailOtp(), and resendEmailOtp()
Forgot/reset passwordIntegratedresetPasswordForEmail(), recovery OTP, then updateUser { password = ... }
Change emailIntegratedupdateUser { email = ... }, then email-change OTP verification
Change passwordIntegratedupdateUser { password = ... }
Sign out other sessionsIntegratedauth.signOut(SignOutScope.OTHERS)
Google Sign-InIntegratedAndroid Credential Manager -> Google ID token -> Supabase ID-token sign-in
Delete accountPartialClient-side row cleanup and local sign-out only; see the honesty note below

Key files

Session state

SupabaseClientProvider installs the Auth plugin with:

install(Auth) {
    scheme = "soar"
    host = "auth-callback"
}

That is why Supabase Auth must allow-list soar://auth-callback. The SessionController observes supabaseClient.auth.sessionStatus, maps SessionStatus.Authenticated to UserInfo?, and exposes both currentUser and isLoading as eager StateFlows scoped to the application.

The app code does not manually persist tokens. It relies on the Supabase Android Auth plugin's SettingsSessionManager, backed by the multiplatform Settings API, and calls auth.awaitInitialization() at startup so the auth gate can wait for the restored session state.

Email flows

The auth screens use pure Kotlin validators from feature/auth/validation/:

  • validateEmail() requires a non-empty syntactic email.
  • validatePassword() requires at least eight characters and both a letter and a number.
  • validatePasswordConfirmation() checks the confirmation field.
  • validateOtp() requires a six-digit code.

AuthRepository wraps Supabase calls in AppResult<Unit>. One-shot operations such as sign-in, sign-up, OTP verification, password changes, and password reset completion are attempted once. Idempotent or request-style operations such as resending email OTP, requesting password reset, changing email, signing out other sessions, and the current delete-account routine are retried up to three times with a 250 ms delay.

Google Sign-In

Google Sign-In is hidden until this key is present:

GOOGLE_WEB_CLIENT_ID=your_google_oauth_web_client_id

Use the OAuth web client ID from Google Cloud Console, not the Android OAuth client ID. CredentialManagerGoogleSignInController builds a GetGoogleIdOption, sets that web client ID as the server client ID, adds a SHA-256 nonce, and returns a Google ID token plus the raw nonce. SupabaseAuthRemoteDataSource.signInWithGoogle() exchanges those through Supabase's IDToken provider with provider = Google.

Credential Manager outcomes are handled explicitly:

ResultApp behavior
Success(idToken, rawNonce)Sign in through Supabase
CancelledTreat as a no-op
NoCredentialSurface that no Google account is available
Failure(message)Show the mapped failure

Enable the Google provider in Supabase Auth and keep the same redirect URL:

soar://auth-callback

Account security actions

AccountSecurityViewModel drives the profile security screen. It can:

  • Request an email change and verify the OTP sent to the new address.
  • Change the current password.
  • Sign out other sessions with SignOutScope.OTHERS.
  • Trigger the current delete-account routine.

Delete account honesty note

The Kotlin template does not currently ship a delete-account Edge Function. SupabaseAuthRemoteDataSource.deleteAccount() does not delete the auth.users row.

The implemented flow is client-side:

  1. Read the current Supabase user id.
  2. Delete that user's rows from public.todos.
  3. Update public.profiles for the same id by setting full_name, bio, phone, location, and avatar_url to null.
  4. Call auth.signOut(SignOutScope.LOCAL).

The account can no longer use local app state, but the Supabase Auth user still exists. Storage objects in sample-uploads/<user-id>/ are not cleaned up, and device-token / billing rows are not explicitly removed by this client-side routine.

Template fix recommended: port the Swift/Expo delete-account Edge Function into the Kotlin template. A service-role function can verify the caller's JWT, remove owned rows and storage objects, and then call auth.admin.deleteUser(userId). This matters for Google Play account deletion policy.

Verify

  • Sign up with email/password and complete the six-digit OTP.
  • Sign out, then sign back in with email/password.
  • Request password recovery and complete the recovery OTP flow.
  • Add GOOGLE_WEB_CLIENT_ID, enable Supabase Google Auth, rebuild, and confirm the Google button appears.
  • Change email and password from Account Security.
  • Confirm delete account behaves exactly as documented until the template fix is added.

On this page