# Authentication Clank auth is built into the same zero-dependency SQLite, Fetch, SSR, and live-query layer as the rest of the framework. An authenticated application needs one auth definition, owned tables for private data, and the auth-first browser client. ## Minimal setup ```ts import { defineAuth, defineBackend, defineDatabase, defineTable, s, } from "@clank.run/framework"; export const auth = defineAuth(); export const schema = defineDatabase({ todos: defineTable({ title: s.string({ min: 1, max: 160 }), done: s.boolean(), }).owned(), }); export const backend = defineBackend({ schema, auth }) .functions(({ query, mutation }) => ({ todos: { list: query({ args: {}, handler: ({ db }) => db.table("todos").collect(), }), add: mutation({ args: { title: s.string({ min: 1, max: 160 }) }, handler: ({ db }, { title }) => db.table("todos").insert({ title, done: false }), }), }, })); ``` When `auth` is present, `query` and `mutation` require a signed-in user by default. Their context includes non-null `user`, `auth`, and the correctly scoped `db`. Use `publicQuery` or `publicMutation` only when anonymous access is intentional. An `.owned()` table automatically writes the current user ID on insert and adds that owner condition to every get, query, update, and delete. A user cannot address another user's row even if they learn its document ID. ## Browser client ```tsx import { AuthGate, createClient, onCleanup } from "@clank.run/framework"; import type { backend } from "./backend.ts"; const client = createClient(); function Todos() { const todos = client.live(client.api.todos.list); onCleanup(() => todos.dispose()); return ( ); } function App() { return ( ); } ``` `createClient()` creates all of the authenticated browser mechanics together: - a zero-codegen typed `client.api` tree; - reactive user, session, loading, and error state; - registration, login, logout, logout-all, and password-change methods; - CSRF headers on authenticated mutations; - one-shot query and mutation calls; - seeded live queries over EventSource. It intentionally accepts the backend as a type argument rather than a runtime value. This prevents server configuration such as a password pepper from entering a browser module. `AuthGate` renders a default accessible email/password screen for the default profile. Pass `signedOut` when an application has custom profile fields or wants a branded sign-in experience. ## Auth client API ```ts client.auth.user.value; client.auth.session.value; client.auth.authenticated.value; client.auth.loading.value; client.auth.error.value; await client.auth.register({ email: "ada@example.com", password: "a long unique passphrase", profile: { name: "Ada" }, }); await client.auth.login({ email, password }); await client.auth.changePassword({ currentPassword, newPassword }); await client.auth.logout(); await client.auth.logoutAll(); await client.auth.reload(); ``` The session credential is never exposed to JavaScript. The browser stores it only as an `HttpOnly` cookie. The CSRF token is held in memory and may be included in script-safe SSR boot state. ## SSR Resolve the request before running private queries: ```tsx const caller = await runtime.caller(request); if (!caller.auth) throw new Error("Auth was not initialized."); const bootAuth = authState(caller.auth); const initial = caller.auth.user ? caller.query(api.todos.list) : { value: [], version: runtime.version }; const page = await renderDocument(view, { state: { auth: bootAuth, todos: initial.value, version: initial.version, }, scripts: ["/app.js"], nonce, }); ``` In the browser: ```ts const initial = readState()!; const client = createClient({ initialAuth: initial.auth, }); client.seed(client.api.todos.list, {}, initial.todos, initial.version); ``` `authState()` selects only the serializable user, session metadata, and CSRF token. Never serialize cookies, password hashes, peppers, database handles, or raw request headers. ## Profiles and roles The default profile is `{ name?: string }`. Define a custom runtime-validated profile when needed: ```ts const auth = defineAuth({ profile: { displayName: s.string({ min: 1, max: 80 }), timezone: s.string({ max: 80 }), }, defaultRole: "member", }); ``` Inside a handler: ```ts handler: ({ auth, user }) => { auth.requireRole("admin", "owner"); return user.profile.displayName; } ``` Trusted server code can call: ```ts runtime.auth.setRole(userId, "admin"); runtime.auth.disableUser(userId); runtime.auth.revokeUserSessions(userId); ``` Disabling a user deletes their sessions. Role changes and session revocations notify active live streams so stale connections close and reconnect through fresh authorization. ## Configuration ```ts const auth = defineAuth({ signup: true, defaultRole: "user", sessionDurationMs: 30 * 24 * 60 * 60 * 1000, idleTimeoutMs: 7 * 24 * 60 * 60 * 1000, touchIntervalMs: 5 * 60 * 1000, cookie: { secure: "auto", sameSite: "Strict", }, password: { minLength: 8, pepper: process.env.CLANK_AUTH_PEPPER, }, rateLimit: { attempts: 10, windowMs: 10 * 60 * 1000, }, }); ``` The default and lowest supported password length is eight characters. Defaults use scrypt with `N=2^17`, `r=8`, and `p=1`, a random 128-bit salt, a 64-byte derived key, constant-time comparison, and bounded concurrent hashing. A pepper is optional and must remain server-only. Changing or losing it invalidates existing password hashes unless the application implements an explicit migration. Authentication attempts are rate-limited in process memory by normalized email and the trusted client identity supplied out of band by Clank's Node adapter. Caller headers cannot select this identity. A custom adapter may provide `rateLimit.clientKey(request)` from adapter-authenticated metadata; a multi-instance deployment also needs a shared `rateLimit.store`. ## HTTP endpoints With the default backend prefix: | Endpoint | Method | Purpose | | --- | --- | --- | | `/__clank/auth/session` | `GET` | Current safe auth state | | `/__clank/auth/register` | `POST` | Create account and session | | `/__clank/auth/login` | `POST` | Verify credentials and create session | | `/__clank/auth/mfa/verify` | `POST` | Complete an email-code MFA challenge | | `/__clank/auth/email/verify` | `POST` | Consume a single-use email-verification token | | `/__clank/auth/email/resend` | `POST` | Issue another verification message | | `/__clank/auth/password/recover` | `POST` | Request a generic password-recovery response | | `/__clank/auth/password/reset` | `POST` | Consume a recovery token and revoke old sessions | | `/__clank/auth/passkeys` | `GET` | List the current verified user's passkeys | | `/__clank/auth/passkeys/register/start` | `POST` | Start passkey registration | | `/__clank/auth/passkeys/register/finish` | `POST` | Verify and store a passkey | | `/__clank/auth/passkeys/authenticate/start` | `POST` | Start discoverable passkey authentication without account lookup | | `/__clank/auth/passkeys/authenticate/finish` | `POST` | Verify an assertion and create a session | | `/__clank/auth/passkeys/delete` | `POST` | Remove one owned passkey | | `/__clank/auth/logout` | `POST` | Revoke the current session | | `/__clank/auth/logout-all` | `POST` | Revoke every session for the user | | `/__clank/auth/change-password` | `POST` | Verify current password, rotate hash, revoke sessions | JSON content type and body limits are enforced. State-changing authenticated requests require the exact-origin check and `x-clank-csrf`. Responses are `no-store`. ## Security boundaries and current scope Built-in auth provides email/password registration, sessions, CSRF protection, roles, owned data, verification, generic recovery, email-code MFA, WebAuthn passkeys, bot-verification hooks, and revocation. Applications supply their email transport and can supply a shared rate-limit store. OAuth/social identity, enterprise federation, hardware-attestation policy, risk scoring, account-linking policy, and provider-specific bot challenges remain integrations. Organization membership and CLI project authority belong to the deployment platform rather than application auth. Continue with [Advanced authentication](authentication.md) for delivery hooks, MFA, passkeys, bot protection, distributed rate limits, and origin troubleshooting. See [Security](security.md) for deployment requirements and [the authenticated Todo](../examples/auth-todo/backend.ts) for the complete working example.