Authentication with Better Auth¶
The starter kit uses Better Auth for authentication and authorization. Better Auth was chosen as it's considered the best TypeScript authentication library currently available, is free and open-source and you keep control of your own data.
Better Auth owns users, accounts, sessions, and verification records. Keep application data in separate Mongoose models, and do not create a second authentication User model.
Configure authentication¶
Environment¶
After copying .env.example to .env, the local URLs are already correct. For a normal
local setup, the only value you need to add is BETTER_AUTH_SECRET:
Generate the value with the Node.js command in Development workflow.
Generate it once, paste the result into .env, and keep it private. Change the other
values in .env.example only when you are using a different MongoDB server, local URL,
or deployment environment. APP_URL and BETTER_AUTH_URL must match in this
same-origin starter.
Project structure¶
apps/server/src/
auth.ts # Better Auth server instance and plugin configuration
middleware/
auth.ts # Express session middleware
apps/client/src/
lib/
auth-client.ts # Better Auth client
Server configuration¶
The server creates the Better Auth instance in apps/server/src/auth.ts:
import { betterAuth } from "better-auth";
import { mongodbAdapter } from "better-auth/adapters/mongodb";
import mongoose from "mongoose";
import type { Config } from "./config.ts";
export function createAuth(config: Config) {
const db = mongoose.connection.db;
if (!db) throw new Error("Connect to MongoDB before initializing authentication");
return betterAuth({
appName: "MERN starter",
database: mongodbAdapter(db),
baseURL: config.authUrl,
secret: config.secret,
trustedOrigins: [config.appUrl],
advanced: { ipAddress: { ipAddressHeaders: ["x-mern-client-ip"] } },
emailAndPassword: { enabled: true, minPasswordLength: 8, maxPasswordLength: 128 },
rateLimit: { enabled: true, storage: "database" },
});
}
Keep the existing URL, secret, trusted origin, client IP handling, password limits, and rate limit when you add a feature. The standalone local MongoDB server has no multi-document transactions; use a replica set and pass its client to the adapter if a later feature needs them.
Client configuration¶
The client lives in apps/client/src/lib/auth-client.ts:
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient();
Use sessions¶
In components¶
Use the client where a component needs the current session:
import { authClient } from "@/lib/auth-client";
function Profile() {
const { data: session, isPending } = authClient.useSession();
if (isPending) return <p role="status">Loading session...</p>;
if (!session) return <p>Sign in to continue.</p>;
return <div>Welcome, {session.user.name}</div>;
}
In Express routes¶
Protect Express routes with authMiddleware:
import { authMiddleware } from "./middleware/auth.ts";
app.get("/api/me", authMiddleware(auth), (_req, res) => {
const { id, name, email } = res.locals.session.user;
res.json({ user: { id, name, email } });
});
authMiddleware(auth) reads the cookie, returns 401 when there is no valid session,
and attaches the session to res.locals for the route handler.
See Middleware for the full implementation.
Handler order and client IP¶
apps/server/src/app.ts mounts /api/auth/{*path} before express.json() so Better Auth receives the original request body. The client uses the same public origin and sends session cookies with its /api requests.
The starter enables database-backed rate limiting. It passes a trusted client IP header built after Express applies the configured TRUST_PROXY list. Only set that list to proxies you control.
Extensions and Plugins¶
The starter kit is configured with Better Auth's email and password flow. Better Auth provides many plugins for common account features. Add the ones your product needs, then build the matching UI and email flow where applicable.
import { admin, organization, twoFactor, username } from "better-auth/plugins";
// Inside the existing betterAuth({ ... }) options:
plugins: [
username(), // A username in addition to email.
twoFactor(), // Authenticator-app two-factor sign-in.
organization(), // Workspaces, members, roles, and invitations.
admin(), // Administrative user-management actions.
],
Other common additions include social sign-in, magic links, passkeys, email verification, and password-reset emails.
Next step¶
Continue to Validation to define trusted input for the widget example.