Auth & user flow
EXEPERT uses Better Auth for identity and sessions. Supabase remains the data plane: Postgres, RLS, Realtime, RPCs, and Edge Functions. The 3D simulation is still available offline and no guest identity is created merely by opening the app.
The main modules are:
api/_lib/auth.ts: Better Auth, Google, verified email/password, CAPTCHA, rate limits, and the native OAuth 2.1 provider.api/_lib/identity.tsandapi/_lib/bridge.ts: stable principal resolution, merge/legacy claims, and five-minute Supabase JWT issuance.src/auth/session.ts: browser member/guest actions and compatibility mode.src/auth/bridge.tsandsrc/data/client.ts: in-memory bridge-token caching and injection into Supabase requests.src/auth/gate.tsandsrc/ui/signin-view.ts: accessible sign-in modal and signed-in account popover.desktop/exepert-desktop/src/chat.rs: hosted PKCE sign-in and token refresh.desktop/exepert-desktop/src/auth_vault.rs: Windows Credential Manager storage for the native refresh token.
Identity boundary
The principal UUID is the durable data owner. A Better Auth user UUID is the
replaceable login identity. Existing Supabase users are seeded as principals
with the same UUID, so current RLS policies can continue comparing owner fields
with auth.uid().
Only the server can read or write authn and app_private. Browser clients
retain the public Supabase URL and publishable key but never receive database
credentials, the Better Auth secret, email credentials, or signing private
key.
Browser states
| State | Session | Data behavior | Account surface |
|---|---|---|---|
| Signed out | None | Simulation stays local; cloud action requests identity | Centered sign-in modal |
| Guest | Better Auth anonymous session | Principal owns temporary cloud work | Upgrade notice in the modal |
| Member | Google or verified email session | Principal owns synchronized work | Compact account popover |
The browser stores the Better Auth session in an HttpOnly, same-site cookie. It requests a Supabase bridge token only when the data client needs one, keeps that token in memory, and refreshes before expiry. It does not persist the bridge JWT in local storage.
Sign-in modal
The avatar in #authSlot opens a centered modal while signed out or using a
guest. It offers Google first, then email sign-in, account creation, password
reset, and the explicit guest action. Escape and the scrim close it, Tab is
trapped within it, initial focus moves inside, and closing restores focus to
the opener. On a narrow viewport the panel becomes a safe-area-aware sheet.
After member sign-in, the same avatar opens a compact non-modal popover with the resolved account and sign-out action. Outside click closes only that popover.
Guest upgrade and legacy claim
Before a guest signs into a member account, the server creates a random,
single-use, 15-minute merge ticket and stores only its SHA-256 hash. The locked
database procedure either keeps the guest principal UUID or moves all known
owned rows to the existing member principal. Colliding project names receive a
deterministic Guest import suffix; rows are not deleted.
During the guarded rollout, the browser can also prove an existing Supabase session to claim its legacy principal. Unclaimed users and guests remain in the database indefinitely. Edge Functions temporarily accept either an explicitly verified Better Auth bridge token or a legacy Supabase session.
Native desktop flow
The native client is public and has no client secret. Loopback ports are selected at runtime, PKCE S256 is mandatory, and the authorization response's state and RFC 9207 issuer must match. The access and bridge tokens remain in memory. Only the rotating refresh token is persisted, under the EXEPERT target in Windows Credential Manager; it is revoked and deleted on sign-out.
Rollback mode
AUTH_RUNTIME_MODE=legacy keeps the browser on Supabase Auth while the new API
and migration are deployed. VITE_AUTH_RUNTIME_MODE is only the offline
fallback for the public config endpoint. The dual Edge verifier and preserved
legacy users make switching the mode back non-destructive. See the
Better Auth rollout runbook before changing the
production flags.