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
bun add @timbl/adapter-better-auth better-authcreateBetterAuthAdapter
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)
| Option | Role |
|---|---|
betterAuth | Return value of betterAuth(...) |
principalSchema | Zod schema for your mapped principal |
mapPrincipal | Map Better Auth getSession result → principal or null |
getIdentity | Principal → AuthIdentity |
serializePrincipal | Optional redaction / shaping per audience |
getCapabilities | Optional extra capability fields merged onto defaults |
invoke | Optional 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.