guide

Auth & sessions

People sign in with a sign-in code sent to their email. There are no passwords anywhere in the system, so there is no password database to leak in a breach.

Sign-in API

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 } }; the token is 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 and subscription are all deleted

Where the code comes from

Until you add a domain, the code comes from Gemmein's shared address as <App name> (via Gemmein). This is the only email Gemmein sends for you. Add your domain on the Domains page for email (or both). The page lists every record it needs, each with a status: the ownership TXT, the sending records and the receiving MX. Once they are found, the code comes from your domain's account address. Sender addresses, replies and the Inbox are covered in Email.

Rate limits and the resend button

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. If someone taps a “Didn't get it? Resend” button twice, they are locked out of their own sign-in for up to 15 minutes. An automatic resend on timeout makes this worse. Disable the button between attempts, show the countdown, and let them wait for the code, which is almost certainly in their inbox already. The 429 carries resetAt, so you can show the exact wait.

Sessions

Sessions persist across page reloads automatically (localStorage in browsers, memory elsewhere; pass a tokenStore for custom persistence). Each person has one session: verifying a new code revokes that email's older sessions on every platform, so signing in on a phone signs the browser out, and the other way round. A stale token throws auth_expired once and the SDK clears it; a retry (or signing in again) recovers.

currentUser() never throws for session state. Call it unguarded on app load. An expired or missing session resolves to { authenticated: false } and does not throw.

The two user shapes

Two calls return the signed-in person, in different shapes. This is the 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 }

On currentUser(), use user.userId. user.id is undefined there, and feeding it into a keyed create (key: "profile:" + user.id) puts every person on one profile:undefined record. Use currentUser().userId as the signed-in identity.

Sign out

The method is g.auth.logout(). There is no signOut: a guessed signOut() inside a try/catch never throws, and leaves the server session live.

Account deletion

account.delete() erases the signed-in person in full: sessions revoked, their records and files deleted, their subscription row removed. This is the GDPR erasure path a person runs themselves, and it needs nothing from you in the dashboard. It cannot be undone, so put a confirmation step in front of it.

currentUser() returns authenticated: false for signed-out, suspended and erased people alike, on purpose. A moderated person's state never reaches the client, and one signed-out screen covers all three.

From your own server

Gemmein runs none of your code. When your own function runs elsewhere (a model call, a PDF render, a nightly job), it can verify the session from any host: pass the browser's session token to gemmeinServer(sk).verifySession(token). One call returns who the person is and what they have access to.

const server = gemmeinServer("sk_...")   // on your server only
const { person, holdings } = await server.verifySession(token)
// person: { id, email, role }; holdings: what they have access to

Always verify the token; never trust a person id the browser sends you. A refusal names the reason (session_invalid, session_expired, session_revoked, person_suspended), so your function can tell “sign in again” apart from “you turned this person off”. See the SDK reference for the server client's methods.

Emailing your people

notify() emails one verified person of your app by id, never by address (the address comes from the server's own record); an id that is not one of your people returns 404 not_a_customer. It sends only from your own verified sender domain: until you verify one on the Domains page, every call returns 409 sender_domain_required and nothing is sent, recorded or counted against a cap. It never falls back to a Gemmein address.

await server.notify("usr_abc123", {
  subject: "Your order shipped",
  text: "Order #142 left the warehouse today.",
  key: "order-142-shipped",   // retries can't double-send
})

Sender addresses, caps and replies are covered in Email. To email many people, or on a webhook, use a relay's email_person.