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, …) fromgetIdentity(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.