guide

Auth & sessions

Users sign in with a one-time email code. There are no passwords anywhere in the system — nothing to store, reset, or breach.

The whole surface

await g.auth.sendEmailCode("user@example.com")   // emails an 8-digit code

const session = await g.auth.verifyEmailCode({ email: "user@example.com", code: "12345678" })
// { token, expiresAt, user: { id, email } } — token stored automatically

const user = await g.auth.currentUser()
// { authenticated: true, userId: "usr_...", email: "user@example.com" }

await g.auth.logout()          // revokes the server session
await g.account.delete()   // GDPR erasure: sessions, records, files, subscription — all gone

Sessions

Sessions persist across page reloads automatically (localStorage in browsers, memory elsewhere — override with tokenStore for custom persistence). One session per user: verifying a new code revokes that email's older sessions. A stale token throws auth_expired once, the SDK clears it, and a retry (or re-auth) recovers.

currentUser() never throws for session state. Call it unguarded on app load — an expired or missing session is the answer { authenticated: false }, not an error.

Rate limits — build the resend button carefully

Sending a code and verifying a code are each capped at 3 per email per 15 minutes (plus 20 and 30 per IP respectively). Codes live for 10 minutes; sessions for 30 days.

Three sends means the original plus two resends. A “Didn't get it? Resend” button that a frustrated person taps twice locks them out of their own sign-in for up to 15 minutes — and an automatic resend-on-timeout makes it strictly worse. Disable the button between attempts, show the countdown, and let them wait for the code that is almost certainly already in their inbox. The 429 carries resetAt, so you can render the exact wait rather than guessing.

The two user shapes — read this twice

Two calls give you "the user", and their shapes differ. This is the single most common auth mistake in AI-written code:

// verifyEmailCode resolves the SESSION — the user is nested, and the field is `id`:
{ token, expiresAt, user: { id, email } }

// currentUser resolves the IDENTITY — flat, and the field is `userId`:
{ authenticated: true, userId, email }

It's user.userId, not user.id, on currentUser(). user.id is undefined there — and feeding it into a keyed create (key: "profile:" + user.id) collapses every user onto one profile:undefined record. Use currentUser().userId as the canonical signed-in identity.

Sign out

It's g.auth.logout() — there is no signOut. A guessed signOut() fails silently in sloppy code and leaves the server session live.

Account deletion

account.delete() erases the signed-in user — the full cascade: sessions revoked, their records and files deleted, their subscription row removed. It's the user-side GDPR erasure path — no dashboard involvement needed. Irreversible; put a real confirm in front of it. And note currentUser() answers authenticated: false identically for signed-out, suspended, and erased — deliberately silent, so one signed-out screen covers all three.