{"protocol":"clank-doc/1","frameworkVersion":"0.8.0","slug":"auth","title":"Authentication","description":"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 clien","group":{"id":"full-stack","title":"Full stack"},"url":"https://docs.clank.run/docs/auth","source":"docs/auth.md","headings":["Minimal setup","Browser client","Auth client API","SSR","Profiles and roles","Configuration","HTTP endpoints","Security boundaries and current scope"],"tableOfContents":[{"id":"minimal-setup","title":"Minimal setup","level":2},{"id":"browser-client","title":"Browser client","level":2},{"id":"auth-client-api","title":"Auth client API","level":2},{"id":"ssr","title":"SSR","level":2},{"id":"profiles-and-roles","title":"Profiles and roles","level":2},{"id":"configuration","title":"Configuration","level":2},{"id":"http-endpoints","title":"HTTP endpoints","level":2},{"id":"security-boundaries-and-current-scope","title":"Security boundaries and current scope","level":2}],"markdown":"# Authentication\n\nClank 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.\n\n## Minimal setup\n\n```ts\nimport {\n  defineAuth,\n  defineBackend,\n  defineDatabase,\n  defineTable,\n  s,\n} from \"@clank.run/framework\";\n\nexport const auth = defineAuth();\n\nexport const schema = defineDatabase({\n  todos: defineTable({\n    title: s.string({ min: 1, max: 160 }),\n    done: s.boolean(),\n  }).owned(),\n});\n\nexport const backend = defineBackend({ schema, auth })\n  .functions(({ query, mutation }) => ({\n    todos: {\n      list: query({\n        args: {},\n        handler: ({ db }) => db.table(\"todos\").collect(),\n      }),\n      add: mutation({\n        args: { title: s.string({ min: 1, max: 160 }) },\n        handler: ({ db }, { title }) =>\n          db.table(\"todos\").insert({ title, done: false }),\n      }),\n    },\n  }));\n```\n\nWhen `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.\n\nAn `.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.\n\n## Browser client\n\n```tsx\nimport { AuthGate, createClient, onCleanup } from \"@clank.run/framework\";\nimport type { backend } from \"./backend.ts\";\n\nconst client = createClient<typeof backend>();\n\nfunction Todos() {\n  const todos = client.live(client.api.todos.list);\n  onCleanup(() => todos.dispose());\n\n  return (\n    <button onClick={() =>\n      client.mutate(client.api.todos.add, { title: \"Private task\" })\n    }>\n      Add\n    </button>\n  );\n}\n\nfunction App() {\n  return (\n    <AuthGate auth={client.auth}>\n      <Todos />\n    </AuthGate>\n  );\n}\n```\n\n`createClient<typeof backend>()` creates all of the authenticated browser mechanics together:\n\n- a zero-codegen typed `client.api` tree;\n- reactive user, session, loading, and error state;\n- registration, login, logout, logout-all, and password-change methods;\n- CSRF headers on authenticated mutations;\n- one-shot query and mutation calls;\n- seeded live queries over EventSource.\n\nIt 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.\n\n`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.\n\n## Auth client API\n\n```ts\nclient.auth.user.value;\nclient.auth.session.value;\nclient.auth.authenticated.value;\nclient.auth.loading.value;\nclient.auth.error.value;\n\nawait client.auth.register({\n  email: \"ada@example.com\",\n  password: \"a long unique passphrase\",\n  profile: { name: \"Ada\" },\n});\nawait client.auth.login({ email, password });\nawait client.auth.changePassword({ currentPassword, newPassword });\nawait client.auth.logout();\nawait client.auth.logoutAll();\nawait client.auth.reload();\n```\n\nThe 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.\n\n## SSR\n\nResolve the request before running private queries:\n\n```tsx\nconst caller = await runtime.caller(request);\nif (!caller.auth) throw new Error(\"Auth was not initialized.\");\n\nconst bootAuth = authState(caller.auth);\nconst initial = caller.auth.user\n  ? caller.query(api.todos.list)\n  : { value: [], version: runtime.version };\n\nconst page = await renderDocument(view, {\n  state: {\n    auth: bootAuth,\n    todos: initial.value,\n    version: initial.version,\n  },\n  scripts: [\"/app.js\"],\n  nonce,\n});\n```\n\nIn the browser:\n\n```ts\nconst initial = readState<PageState>()!;\nconst client = createClient<typeof backend>({\n  initialAuth: initial.auth,\n});\nclient.seed(client.api.todos.list, {}, initial.todos, initial.version);\n```\n\n`authState()` selects only the serializable user, session metadata, and CSRF token. Never serialize cookies, password hashes, peppers, database handles, or raw request headers.\n\n## Profiles and roles\n\nThe default profile is `{ name?: string }`. Define a custom runtime-validated profile when needed:\n\n```ts\nconst auth = defineAuth({\n  profile: {\n    displayName: s.string({ min: 1, max: 80 }),\n    timezone: s.string({ max: 80 }),\n  },\n  defaultRole: \"member\",\n});\n```\n\nInside a handler:\n\n```ts\nhandler: ({ auth, user }) => {\n  auth.requireRole(\"admin\", \"owner\");\n  return user.profile.displayName;\n}\n```\n\nTrusted server code can call:\n\n```ts\nruntime.auth.setRole(userId, \"admin\");\nruntime.auth.disableUser(userId);\nruntime.auth.revokeUserSessions(userId);\n```\n\nDisabling a user deletes their sessions. Role changes and session revocations notify active live streams so stale connections close and reconnect through fresh authorization.\n\n## Configuration\n\n```ts\nconst auth = defineAuth({\n  signup: true,\n  defaultRole: \"user\",\n  sessionDurationMs: 30 * 24 * 60 * 60 * 1000,\n  idleTimeoutMs: 7 * 24 * 60 * 60 * 1000,\n  touchIntervalMs: 5 * 60 * 1000,\n  cookie: {\n    secure: \"auto\",\n    sameSite: \"Strict\",\n  },\n  password: {\n    minLength: 8,\n    pepper: process.env.CLANK_AUTH_PEPPER,\n  },\n  rateLimit: {\n    attempts: 10,\n    windowMs: 10 * 60 * 1000,\n  },\n});\n```\n\nThe 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.\n\nAuthentication 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`.\n\n## HTTP endpoints\n\nWith the default backend prefix:\n\n| Endpoint | Method | Purpose |\n| --- | --- | --- |\n| `/__clank/auth/session` | `GET` | Current safe auth state |\n| `/__clank/auth/register` | `POST` | Create account and session |\n| `/__clank/auth/login` | `POST` | Verify credentials and create session |\n| `/__clank/auth/mfa/verify` | `POST` | Complete an email-code MFA challenge |\n| `/__clank/auth/email/verify` | `POST` | Consume a single-use email-verification token |\n| `/__clank/auth/email/resend` | `POST` | Issue another verification message |\n| `/__clank/auth/password/recover` | `POST` | Request a generic password-recovery response |\n| `/__clank/auth/password/reset` | `POST` | Consume a recovery token and revoke old sessions |\n| `/__clank/auth/passkeys` | `GET` | List the current verified user's passkeys |\n| `/__clank/auth/passkeys/register/start` | `POST` | Start passkey registration |\n| `/__clank/auth/passkeys/register/finish` | `POST` | Verify and store a passkey |\n| `/__clank/auth/passkeys/authenticate/start` | `POST` | Start discoverable passkey authentication without account lookup |\n| `/__clank/auth/passkeys/authenticate/finish` | `POST` | Verify an assertion and create a session |\n| `/__clank/auth/passkeys/delete` | `POST` | Remove one owned passkey |\n| `/__clank/auth/logout` | `POST` | Revoke the current session |\n| `/__clank/auth/logout-all` | `POST` | Revoke every session for the user |\n| `/__clank/auth/change-password` | `POST` | Verify current password, rotate hash, revoke sessions |\n\nJSON content type and body limits are enforced. State-changing authenticated requests require the exact-origin check and `x-clank-csrf`. Responses are `no-store`.\n\n## Security boundaries and current scope\n\nBuilt-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.\n\nOAuth/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.\n\nContinue 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.\n"}