AUTH STARTER · TWO CONFIGS, ONE TRUTH
Two configs, one truth
The single most important pattern in this template: auth.config.ts is the brain. auth.config.server.ts holds the secrets. Get this and the rest is muscle memory.
We split auth config into two files on purpose. One is safe for client and server. The other holds secrets and is import-guarded so it can never reach the browser. Every other auth file reads from these two — and only these two.
src/auth.config.ts (shared)
Imported by client code AND server code. So it CANNOT touch env vars that aren't public. It holds:
- App identity (appName, baseUrl).
- All route paths (signIn, signUp, afterSignIn, etc.).
- Protected route prefixes — the proxy reads this.
- Plugin toggles (every plugin has an enabled boolean here).
- Email & password config (min length, autoSignIn, etc.).
- Session timing (expiresIn, updateAge, cookie cache).
- Cookie attributes (prefix, sameSite, secure).
Never import env.ts from auth.config.ts
If you do, the server-only env vars (BETTER_AUTH_SECRET, etc.) end up in your client bundle. We have an eslint rule for this, but the rule of thumb is simpler: in auth.config.ts, only process.env.NEXT_PUBLIC_* and process.env.NODE_ENV are allowed.
src/auth.config.server.ts (secrets)
Marked with import "server-only" at the top. Next.js throws a build error if a Client Component tries to import it. This is where all the secret material lives:
- BETTER_AUTH_SECRET (cookie signing key).
- OAuth client IDs and secrets.
- Resend API key and EMAIL_FROM.
- Cloudflare Turnstile secret key.
- Passkey rpID and origin.
- Trusted origins for CORS/redirect safety.
How they come together
// src/lib/auth.ts
import { authConfig } from "@/auth.config"; // shared
import { authServerConfig } from "@/auth.config.server"; // secrets
export const auth = betterAuth({
appName: authConfig.appName,
baseURL: authServerConfig.baseUrl,
secret: authServerConfig.secret,
emailAndPassword: {
enabled: authConfig.emailAndPassword.enabled,
minPasswordLength: authConfig.emailAndPassword.minPasswordLength,
// ...
},
socialProviders, // built from authServerConfig
plugins, // built from authConfig toggles
});The 90% rule
When you want to change auth behavior, the answer is almost always: edit auth.config.ts and restart dev. Renaming /sign-in to /login? routes.signIn. Want 7-day sessions? session.expiresIn. Turn off passkeys? plugins.passkey.enabled = false.
Don't hardcode "/sign-in" anywhere else
Every redirect, link, and proxy rule reads routes from authConfig. If you hardcode "/sign-in" in a component, you'll break things the day you rename the route. Use authConfig.routes.signIn.