Auth Module
JWT authentication for Express, Better Auth for Next.js and TanStack Start
Overview
The Auth module adds user authentication to your project. The implementation is framework-specific:
| Stack | Implementation | Requires database |
|---|---|---|
| Express backend | JWT + bcrypt | No (uses in-memory user store by default) |
| Next.js full-stack | Better Auth | Yes - only prompted when a database is selected |
| TanStack full-stack | Better Auth | Yes - only prompted when a database is selected |
For Next.js and TanStack Start projects, the auth prompt is automatically
skipped if you chose none for the database type. Better Auth requires a
database-backed adapter to function.
Express - JWT Auth
Generated files
src/
|-- lib/
| \-- jwt.ts # JWT sign/verify utilities
|-- middleware/
| \-- requireAuth.ts # Bearer token validation middleware
|-- services/
| \-- userStore.ts # User lookup helper
|-- controllers/
| \-- authController.ts # Register/login request handlers
\-- routes/
\-- auth.ts # Router mounted at /authThe auth router is registered in src/index.ts:
import authRouter from "./routes/auth";
app.use("/auth", authRouter);JWT utility - src/lib/jwt.ts
import jwt from "jsonwebtoken";
export interface AuthTokenPayload {
userId: string;
}
const JWT_SECRET = process.env.JWT_SECRET!;
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "1h";
export function signToken(payload: AuthTokenPayload): string {
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
}
export function verifyToken(token: string): AuthTokenPayload {
return jwt.verify(token, JWT_SECRET) as AuthTokenPayload;
}Auth middleware - src/middleware/requireAuth.ts
import { NextFunction, Request, Response } from "express";
import { AuthTokenPayload, verifyToken } from "../lib/jwt";
export interface AuthenticatedRequest extends Request {
user?: AuthTokenPayload;
}
export function requireAuth(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer")) {
res.status(401).json({ error: "Authorization header missing or invalid" });
return;
}
const token = authHeader.substring("Bearer ".length);
try {
const payload = verifyToken(token);
req.user = payload;
next();
} catch (error) {
console.error("JWT verification failed:", error);
res.status(401).json({ error: "Invalid or expired token" });
}
}The middleware is named requireAuth in the generated file. Import it by
that name when protecting routes.
Environment variables added
JWT_SECRET=please-change-me
JWT_EXPIRES_IN=1hDependencies added
{
"dependencies": {
"bcrypt": "^6.0.0",
"jsonwebtoken": "^9.0.3"
},
"devDependencies": {
"@types/bcrypt": "^6.0.0",
"@types/jsonwebtoken": "^9.0.10"
}
}No additional scripts are added by the auth module.
Next.js - Better Auth
Generated files
lib/
|-- auth.ts # Better Auth server configuration
|-- auth-client.ts # createAuthClient() for client components
|-- auth-schema.ts # Zod schemas and auth error helper
components/
\-- auth/
|-- sign-in-form.tsx # Client login form
|-- sign-up-form.tsx # Client signup form
\-- sign-out-button.tsx # Client sign-out action
app/
|-- sign-in/page.tsx # Session-aware login route
|-- sign-up/page.tsx # Session-aware signup route
|-- dashboard/page.tsx # Protected example page
\-- api/auth/[...all]/route.ts # Better Auth route handlerThe adapter used in lib/auth.ts is determined by your ORM selection:
- Prisma ->
prismaAdapterfrombetter-auth/adapters/prisma - Drizzle ->
drizzleAdapterfrombetter-auth/adapters/drizzle - MongoDB ->
mongodbAdapterfrombetter-auth/adapters/mongodb
Better Auth configuration - lib/auth.ts
import { betterAuth } from "better-auth";
import { nextCookies } from "better-auth/next-js";
import { prismaAdapter } from "better-auth/adapters/prisma";
import client from "./prisma";
export const auth = betterAuth({
database: prismaAdapter(client, { provider: "postgresql" }),
emailAndPassword: { enabled: true },
plugins: [nextCookies()],
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
});Route handler - app/api/auth/[...all]/route.ts
import { toNextJsHandler } from "better-auth/next-js";
import { auth } from "@/lib/auth";
export const { GET, POST } = toNextJsHandler(auth);Client helper - lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient();Usage in a client component:
const result = await authClient.signIn.email({
email: "[email protected]",
password: "password",
});
if (result.error) {
console.error(result.error);
}Generated auth routes
/sign-inredirects to/dashboardwhen the user already has a session/sign-upredirects to/dashboardwhen the user already has a session/dashboardredirects to/sign-inwhen no session exists
The generated forms use React Hook Form + Zod for field validation and Better Auth client helpers for the submit actions.
Schema changes
- Prisma + Better Auth: the generated
prisma/schema.prismaincludes Better Auth core tables. - Drizzle + Better Auth: the generated
lib/schema.tscontains Better Auth core tables instead of a sample table. - MongoDB + Better Auth: authentication uses the native MongoDB adapter and its own collections.
Environment variables added
BETTER_AUTH_SECRET=please-change-me-to-a-random-string
BETTER_AUTH_URL=http://localhost:3000Do not prefix BETTER_AUTH_SECRET or BETTER_AUTH_URL with
NEXT_PUBLIC_. These are server-side variables only.
Dependencies added
{
"dependencies": {
"@hookform/resolvers": "^5.9.1",
"better-auth": "^1.7.3",
"mongodb": "^7.6.0",
"react-hook-form": "^7.87.0",
"zod": "^4.5.4"
}
}TanStack Start - Better Auth
For TanStack Start, sessions use the tanstackStartCookies() plugin and server functions (createServerFn) instead of Next.js route handlers.
Generated files
src/
|-- lib/
| |-- auth.ts # Better Auth server configuration
| |-- auth-client.ts # createAuthClient() for client components
| |-- auth-functions.ts # getSession() server function
| \-- auth-schema.ts # Zod schemas and auth error helper
|-- components/
| \-- auth/
| |-- sign-in-form.tsx # Client login form
| |-- sign-up-form.tsx # Client signup form
| \-- sign-out-button.tsx # Client sign-out action
\-- routes/
|-- sign-in.tsx # Session-aware login route
|-- sign-up.tsx # Session-aware signup route
|-- dashboard.tsx # Protected example page
\-- api/auth/$.ts # Better Auth route handlerThe adapter used in src/lib/auth.ts is determined by your ORM selection:
- Prisma ->
prismaAdapterfrombetter-auth/adapters/prisma - Drizzle ->
drizzleAdapterfrombetter-auth/adapters/drizzle - MongoDB ->
mongodbAdapterfrombetter-auth/adapters/mongodb
Better Auth configuration - src/lib/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { tanstackStartCookies } from "better-auth/tanstack-start";
import { db } from "./db";
export const auth = betterAuth({
database: drizzleAdapter(db, { provider: "pg" }),
emailAndPassword: { enabled: true },
plugins: [tanstackStartCookies()],
secret: process.env.BETTER_AUTH_SECRET,
baseURL: process.env.BETTER_AUTH_URL,
});Auth route handler - src/routes/api/auth/$.ts
import { createFileRoute } from "@tanstack/react-router";
import { auth } from "../../../lib/auth";
export const Route = createFileRoute("/api/auth/$")({
server: {
handlers: {
GET: ({ request }) => auth.handler(request),
POST: ({ request }) => auth.handler(request),
},
},
});Session server function - src/lib/auth-functions.ts
import { createServerFn } from "@tanstack/react-start";
import { getRequestHeaders } from "@tanstack/react-start/server";
import { auth } from "./auth";
export const getSession = createServerFn({ method: "GET" }).handler(
async () => {
return await auth.api.getSession({
headers: getRequestHeaders(),
});
}
);Protected routes call auth.api.getSession in beforeLoad and redirect({ to: "/sign-in" }) when no session exists. The router is wired with setupRouterSsrQueryIntegration in src/router.tsx.
Schema changes
- Prisma + Better Auth: the generated
prisma/schema.prismaincludes Better Auth core tables. - Drizzle + Better Auth: the generated
src/lib/schema.tscontains Better Auth core tables instead of a sample table. - MongoDB + Better Auth: authentication uses the native MongoDB adapter and its own collections.
Environment variables added
BETTER_AUTH_SECRET=please-change-me-to-a-random-string
BETTER_AUTH_URL=http://localhost:3000Do not prefix BETTER_AUTH_SECRET or BETTER_AUTH_URL with anything like
NEXT_PUBLIC_. These are server-side variables only.
Dependencies added
{
"dependencies": {
"@hookform/resolvers": "^5.9.1",
"better-auth": "^1.7.3",
"dotenv": "^17.4.2",
"mongodb": "^7.6.0",
"react-hook-form": "^7.87.0",
"zod": "^4.5.4"
}
}Security recommendations
- Set
JWT_SECRETorBETTER_AUTH_SECRETto a long, random string before deploying - Keep tokens short-lived (
JWT_EXPIRES_IN=1his the generated default) - Use HTTPS in production - never send tokens over plain HTTP
- Add rate limiting to auth endpoints to prevent brute-force attacks
- Never log or store plain-text passwords