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.