Auth & Permissions

Outer wraps Better Auth directly — .auth(config) mounts the handler at /api/auth/** and wires Better Auth's database to whichever dialect you configured on new Outer({ db }).

Enabling auth

new Outer({ db: pglite() }).schema(v1_0).auth({ secret: process.env.AUTH_SECRET! }).build();

.auth() can appear anywhere in the builder chain, and must be called before .build(). Its config is Omit<BetterAuthOptions, "database"> & { secret: string } — every Better Auth option (plugins, emailAndPassword, trustedOrigins, etc.) is accepted directly, with secret made required. database is owned by Outer and cannot be overridden here.

Calling .auth() returns a new Outer whose context.auth type is narrowed to required (non-optional) for everything chained after it. When .auth() is not called, context.auth is undefined, /api/auth/** is not mounted, and resource permissions other than "public" will throw a configuration error.

Outer's core does not set any Better Auth defaults — no default plugins, no default email options. Configure everything explicitly.

baseURL

baseURL defaults to the baseUrl passed to new Outer({ baseUrl }), but can be overridden per-call via .auth({ baseURL }).

It accepts either a static string or Better Auth's DynamicBaseURLConfig, which derives the correct origin per-request from the Host header instead of a fixed value:

.auth({
  secret: process.env.AUTH_SECRET!,
  baseURL: {
    // "*" allows every host — fine for scaffolding/previews, but means
    // anyone can point this server at itself with a spoofed Host header.
    // Once you have a real domain, restrict this to only the hosts you
    // actually serve, e.g. ["yourapp.com", "*.yourapp.com"].
    allowedHosts: ["*"],
    fallback: "http://localhost:3000", // must resolve to a real value — an unset env var here silently disables the fallback
  },
})

Use this for deployments behind a dynamic or preview domain (Vercel previews, StackBlitz, Coolify preview deployments) where the real origin isn't known at build time. A fixed baseUrl there causes Better Auth to scope session cookies to the wrong origin, so sign-in appears to succeed but the session is never actually persisted.

Patterns are matched against the full Host header including port — bare "localhost" will not match "localhost:3000"; use "localhost:*" if you need to restrict rather than allow all hosts.

Reading the session in procedures

Use .middleware() to load the session once and add it to context:

.auth()
.middleware(async ({ context, next }) => {
  const session = await context.auth.api.getSession({ headers: context.headers });
  return next({ context: { user: session?.user } });
})

Fields added via next({ context: { ... } }) are merged into every subsequent .procedure() handler's context.

Permissions on .resource()

.resource() accepts a permissions map per CRUD action — see Schema & Migrations for the full table of "public", "authenticated", "admin", and "owner" behavior, including automatic owner-column injection and 403 handling.

Next: register custom Procedures, or stream updates with Realtime.