Authentication

Flag-gated email auth with secure desktop session storage.

Authentication is Flag-gated through VITE_AUTH_ENABLED, which defaults to true. The template implements Supabase email/password sign-in, sign-up with a six-digit verification code, forgot/reset password with a recovery code, a signed-in Account page, profile editing, and avatar upload.

The current renderer validates VITE_SUPABASE_URL and VITE_SUPABASE_PUBLISHABLE_KEY before mounting, even if VITE_AUTH_ENABLED=false. Use real or placeholder Supabase values during local shell work, and use a real project for auth/data flows.

Runtime gates

GateDefaultEffect
VITE_AUTH_ENABLEDtrueShows auth and account surfaces in navigation.
Supabase env validationRequired todayapp/renderer.tsx renders SupabaseConfigErrorScreen when the URL is missing, the URL is invalid, or the publishable key is missing.
Supabase schemaManual setupprofiles, todos, and avatars are required for account and example data surfaces.

With auth surfaces off, the app is a local desktop shell with no sign-in UI. Billing remains separately gated by VITE_BILLING_ENABLED.

Auth flows

Auth routes live in app/routes/auth/. AuthCard provides the centered shell, and forms use react-hook-form plus Zod schemas from app/lib/auth/auth-schemas.ts.

FlowRouteSupabase call
Sign in/loginsupabase.auth.signInWithPassword
Sign up/registersupabase.auth.signUp with full_name metadata
Verify sign-up/register verify stepsupabase.auth.verifyOtp({ type: 'signup' })
Resend sign-up code/register verify stepsupabase.auth.resend({ type: 'signup' })
Forgot password/forgot-passwordsupabase.auth.resetPasswordForEmail
Reset password/reset-passwordverifyOtp({ type: 'recovery' }), updateUser({ password }), then signOut()
Sign outSidebar / Accountsupabase.auth.signOut() through the auth store

There are no OAuth providers wired in this template today. Adding Google, GitHub, or Apple is a Supabase provider setup plus desktop deep-link routing task; keep the callback route inside the typed app-intent allow-list if you add one.

Session state

app/stores/auth-store.ts is the renderer source of truth for:

  • status: unknown, authenticated, or unauthenticated
  • user and session from Supabase Auth
  • profile from public.profiles
  • profile refresh and sign-out actions

On startup it calls supabase.auth.getSession(), then subscribes to onAuthStateChange. SIGNED_IN, INITIAL_SESSION, TOKEN_REFRESHED, and USER_UPDATED trigger a profile refresh. SIGNED_OUT clears the user, session, and profile.

Secure desktop storage

The Supabase client in app/lib/supabase/client.ts uses a custom storage adapter from secure-session-storage.ts:

Supabase auth storage key
  -> app/lib/supabase/secure-session-storage.ts
  -> window.conveyor.secure
  -> lib/main/secure-storage.ts
  -> Electron safeStorage encrypted file under userData/secrets.bin

All Supabase sessions are stored in the fixed secure slot supabase-session. Older localStorage session keys are removed on startup so tokens do not stay in browser storage.

When secure storage cannot decrypt or read a value, the adapter returns null and Supabase treats the app as signed out. When encryption is unavailable during a write, the main process returns ENCRYPTION_UNAVAILABLE, and the auth write fails instead of silently saving tokens in plaintext.

Account and avatar

The Account page is app/routes/AccountPage.tsx, backed by app/routes/settings/sections/AccountSection.tsx.

SurfaceSourceBackend expectation
Profile loadapp/stores/auth-store.ts, app/services/profile.tsprofiles.id = auth.uid() row exists.
Profile updateupdateProfile(userId, patch)RLS allows the user to update only their own row.
Avatar uploadapp/services/avatar.tsPrivate avatars bucket, 5 MB limit, PNG/JPEG/WebP, path <user-id>/avatar.<ext>.
Avatar displayuseAvatarUrl and createSignedUrlSigned URL TTL is seven days and gets a cache key from profile.updated_at.

Avatar and profile are Example surfaces. They are useful patterns for desktop file picking, Storage upload, signed URL reads, and profile RLS, but you should adapt the fields to your product.

The current email flows are OTP-based, not link-callback based. Supabase sends codes; users enter them into the desktop app. The client also uses detectSessionInUrl: false.

Allow soar-electron:// in Supabase Auth if you later switch to link-based email templates or OAuth, but do not assume /login or /reset-password deep links work today. The current app-intent allow-list accepts shell routes such as /settings, /account, and /billing.

Verify authentication

  • Remove or break VITE_SUPABASE_URL and confirm SupabaseConfigErrorScreen appears before the app mounts.
  • Sign up with email/password, enter the six-digit code, and confirm the app lands on Home.
  • Confirm a profiles row was created with the user's id.
  • Sign out, sign in again, relaunch, and confirm the session persists through secure storage.
  • Run forgot/reset password and confirm the recovery code flow signs the user out after updating the password.
  • Upload and remove an avatar from Account.

On this page