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
| Gate | Default | Effect |
|---|---|---|
VITE_AUTH_ENABLED | true | Shows auth and account surfaces in navigation. |
| Supabase env validation | Required today | app/renderer.tsx renders SupabaseConfigErrorScreen when the URL is missing, the URL is invalid, or the publishable key is missing. |
| Supabase schema | Manual setup | profiles, 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.
| Flow | Route | Supabase call |
|---|---|---|
| Sign in | /login | supabase.auth.signInWithPassword |
| Sign up | /register | supabase.auth.signUp with full_name metadata |
| Verify sign-up | /register verify step | supabase.auth.verifyOtp({ type: 'signup' }) |
| Resend sign-up code | /register verify step | supabase.auth.resend({ type: 'signup' }) |
| Forgot password | /forgot-password | supabase.auth.resetPasswordForEmail |
| Reset password | /reset-password | verifyOtp({ type: 'recovery' }), updateUser({ password }), then signOut() |
| Sign out | Sidebar / Account | supabase.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, orunauthenticateduserandsessionfrom Supabase Authprofilefrompublic.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.binAll 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.
| Surface | Source | Backend expectation |
|---|---|---|
| Profile load | app/stores/auth-store.ts, app/services/profile.ts | profiles.id = auth.uid() row exists. |
| Profile update | updateProfile(userId, patch) | RLS allows the user to update only their own row. |
| Avatar upload | app/services/avatar.ts | Private avatars bucket, 5 MB limit, PNG/JPEG/WebP, path <user-id>/avatar.<ext>. |
| Avatar display | useAvatarUrl and createSignedUrl | Signed 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.
Redirects and deep links
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_URLand confirmSupabaseConfigErrorScreenappears before the app mounts. - Sign up with email/password, enter the six-digit code, and confirm the app lands on Home.
- Confirm a
profilesrow 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.
