Skip to content

Better Auth adapter

The official Better Auth integration. It implements AuthAdapter and is the default auth adapter when you don’t configure one. It lives outside @timbl/core so core stays free of Better Auth dependencies.

Install

Terminal window
bun add @timbl/adapter-better-auth better-auth

createBetterAuthAdapter

The runtime default uses Better Auth with email/password enabled and in-memory storage. Production apps should configure their own Better Auth instance and pass it through adapters.auth for durable users, secrets, plugins, and email policy.

import { betterAuth } from "better-auth";
import { createBetterAuthAdapter } from "@timbl/adapter-better-auth";
import { z } from "zod";
const appAuth = betterAuth({ /* your Better Auth config */ });
const Principal = z.object({
session: z.object({ userId: z.string() }),
user: z.object({ id: z.string(), email: z.string().optional() }),
});
export const authAdapter = createBetterAuthAdapter({
betterAuth: appAuth,
principalSchema: Principal,
mapPrincipal: ({ result }) => result as z.infer<typeof Principal>,
getIdentity: (principal) => ({
subject: principal.user.id,
actorType: "human",
claims: {},
roles: [],
sessionId: principal.session.userId,
}),
invoke: async (action, input, context, instance) => {
// Map Timbl action names to Better Auth API calls; return AuthActionResult shape.
throw new Error(`Unhandled action: ${action}`);
},
});

Options (conceptual)

OptionRole
betterAuthReturn value of betterAuth(...)
principalSchemaZod schema for your mapped principal
mapPrincipalMap Better Auth getSession result → principal or null
getIdentityPrincipal → AuthIdentity
serializePrincipalOptional redaction / shaping per audience
getCapabilitiesOptional extra capability fields merged onto defaults
invokeOptional custom action handler; receives Better Auth instance as 4th argument

Default capabilities include provider: "better-auth", providerRoutes: { enabled: true, basePath: "/api/auth" }, and canonical Timbl actions for login, register, and logout. Backward-compatible aliases such as passwordSignIn, passwordSignUp, and signOut remain callable through the generic action route, but they are not advertised by default so Scalar shows the consistent Timbl action surface.

Wiring in defineCMS

import { defineCMS } from "timbl";
import { authAdapter } from "./auth-adapter";
export default defineCMS({
adapters: { auth: authAdapter },
// collections, plugins, ...
});

The runtime registers /api/auth/* passthrough when handleRequest is available on the adapter. The generated OpenAPI document represents that provider-owned route family as /api/auth/{path} so Scalar exposes the passthrough without claiming Timbl owns Better Auth’s concrete request and response shapes.

Timbl’s own action route remains /api/cms/auth/actions/:action, and advertised actions are displayed as literal routes such as /api/cms/auth/actions/login, /api/cms/auth/actions/register, and /api/cms/auth/actions/logout. Use those for stable application-level auth actions; use /api/auth/* for Better Auth-native endpoints.

See also