Skip to content

Authentication

Timbl treats auth as an adapter: AuthAdapter from @timbl/core (also available via timbl). The timbl runtime installs a default Better Auth adapter when no auth adapter is configured, and the official integration lives in @timbl/adapter-better-auth.

Concepts

  • Principal: opaque value returned by getPrincipal (often validated with Zod).
  • Identity: normalized AuthIdentity (subject, actorType, claims, roles, sessionId, …) from getIdentity(principal).
  • Capabilities: getCapabilities() advertises features (e.g. Better Auth provider routes at /api/auth).

Default auth

When no auth adapter is supplied through defineCMS({ adapters }) or runtime options, Timbl creates a default Better Auth adapter. It enables email/password login, register, and logout actions, mounts Better Auth provider routes at /api/auth/*, and uses in-memory Better Auth storage. Configure your own Better Auth instance for durable users, secrets, email settings, and production policy.

Route protection

Plugin and core routes use auth: "public" | "session". Session routes receive context.auth.principal and context.auth.identity when the adapter resolves a logged-in user.

The generated Scalar docs use the same protection model. Timbl emits OpenAPI security schemes for cookie sessions and bearer tokens, then marks protected operations with those schemes. Public auth routes show auth as optional so sign-in works without a session and sign-out can still send one.

Timbl always shows canonical auth actions as literal OpenAPI routes, so Scalar has a stable surface such as POST /api/cms/auth/actions/login, register, and logout instead of only a generic catch-all. Adapters can advertise actions in their capabilities to refine those schemas or add more routes.

Principal helpers (stable)

Re-exported from timbl:

import {
getIdentity,
isAuthenticated,
isHumanUser,
getSubject,
} from "timbl";
// Inside a route handler:
const id = getIdentity(context.auth.principal);

Custom adapter

Use defineAuthAdapter from timbl / @timbl/core:

import { defineAuthAdapter, validateAuthAdapter } from "timbl";
import { z } from "zod";
const Principal = z.object({ userId: z.string() });
export const myAuth = validateAuthAdapter(
defineAuthAdapter({
principalSchema: Principal,
async getPrincipal({ request }) {
const token = request.headers.get("authorization");
if (!token) return null;
return { userId: "user_1" };
},
getIdentity: (principal) => ({
subject: principal.userId,
actorType: "human",
claims: {},
roles: [],
}),
getCapabilities: () => ({
provider: "custom",
providerRoutes: { enabled: false },
actions: {
login: {
description: "Sign in with email and password.",
inputSchema: z.object({
email: z.string().email(),
password: z.string(),
}),
outputSchema: z.object({ success: z.boolean() }).passthrough(),
},
},
}),
}),
);

Pass adapters: { auth: myAuth } into defineCMS or runtime options to replace the default Better Auth adapter.

Better Auth

See Better Auth adapter and package @timbl/adapter-better-auth.

Auth HTTP surface

Timbl exposes GET /api/cms/auth/session and POST /api/cms/auth/actions/:action. The POST body is forwarded to invoke as provided by the client; the default Better Auth adapter implements login, register, and logout. Custom adapters without invoke return 501 NOT_IMPLEMENTED for auth actions. Advertised actions are additionally shown in the generated docs as concrete routes.

Provider-specific Better Auth routes are served under /api/auth/* by default.

For method-level detail, including the Scalar/OpenAPI security metadata, see Auth API reference.