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
| Surface | Status | Implementation |
|---|---|---|
| Email/password sign-in | Integrated | SupabaseAuthRemoteDataSource.signIn() with the Supabase Email provider |
| Sign-up + OTP verification | Integrated | signUp(), verifyEmailOtp(), and resendEmailOtp() |
| Forgot/reset password | Integrated | resetPasswordForEmail(), recovery OTP, then updateUser { password = ... } |
| Change email | Integrated | updateUser { email = ... }, then email-change OTP verification |
| Change password | Integrated | updateUser { password = ... } |
| Sign out other sessions | Integrated | auth.signOut(SignOutScope.OTHERS) |
| Google Sign-In | Integrated | Android Credential Manager -> Google ID token -> Supabase ID-token sign-in |
| Delete account | Partial | Client-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_idUse 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:
| Result | App behavior |
|---|---|
Success(idToken, rawNonce) | Sign in through Supabase |
Cancelled | Treat as a no-op |
NoCredential | Surface 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-callbackAccount 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:
- Read the current Supabase user id.
- Delete that user's rows from
public.todos. - Update
public.profilesfor the same id by settingfull_name,bio,phone,location, andavatar_urltonull. - 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.
