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.