ZZuro Docs

Auth

Published Oct 3, 2025 | Updated Mar 13, 2026

Add production-ready authentication with Better Auth or JWT

Overview

Add production-ready email/password authentication to your backend.

zuro add auth now supports two providers:

  • better-auth (default): session/cookie auth via Better Auth.
  • jwt: access/refresh token auth with local JWT handlers.

Install

Interactive choice:

npx zuro add auth

Direct provider selection:

npx zuro add auth --auth-provider better-auth
npx zuro add auth --auth-provider jwt

Auth install auto-resolves required module dependencies (database, error-handler) if missing.


What This Generates

Shared files:

src/
├ lib/
│ └ auth.ts
├ controllers/
│ └ user.controller.ts
├ routes/
│ └ user.routes.ts
└ db/schema/
  └ auth.ts       # mysql projects receive mysql-compatible auth schema

JWT-only additional files:

src/
├ controllers/
│ └ auth.controller.ts
└ routes/
  └ auth.routes.ts

Route wiring updates:

  • Better Auth: app.ts mounts /api/auth handler directly.
  • JWT: route mounts are injected for /api/auth and /api/users.

Provider Behavior

Better Auth (default)

  • Uses Better Auth session flow.
  • Session is resolved from cookies.
  • Best if you want managed auth primitives and plugin ecosystem.

JWT

  • Uses signed access + refresh tokens.
  • Access token is read from Authorization: Bearer <token>.
  • Refresh tokens are persisted (hashed) in DB and rotated on refresh.

Configuration

Better Auth env:

BETTER_AUTH_SECRET=your-secret-key-at-least-32-characters-long
BETTER_AUTH_URL=http://localhost:3000
DATABASE_URL=postgresql://postgres@localhost:5432/mydb

JWT env:

JWT_ACCESS_SECRET=your-jwt-access-secret-at-least-32-characters
JWT_REFRESH_SECRET=your-jwt-refresh-secret-at-least-32-characters
JWT_ACCESS_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
DATABASE_URL=postgresql://postgres@localhost:5432/mydb

API Reference

Better Auth endpoints:

  • POST /api/auth/sign-up/email
  • POST /api/auth/sign-in/email
  • POST /api/auth/sign-out
  • GET /api/auth/get-session
  • GET /api/users/me

JWT endpoints:

  • POST /api/auth/sign-up/email
  • POST /api/auth/sign-in/email
  • POST /api/auth/refresh
  • POST /api/auth/sign-out
  • GET /api/auth/get-session
  • GET /api/users/me

Quick Example (JWT)

curl -X POST http://localhost:3000/api/auth/sign-in/email \
  -H "Content-Type: application/json" \
  -d '{
    "email": "ava@example.com",
    "password": "password123"
  }'

Use returned accessToken as bearer token:

GET /api/users/me
Authorization: Bearer <access-token>

Run Migrations

npx drizzle-kit generate
npx drizzle-kit migrate

Protecting Routes

Use the generated auth middleware to guard your routes.

Better Auth — protect a route with session check:

// src/middleware/require-auth.ts
import { auth } from "../lib/auth";
import type { Request, Response, NextFunction } from "express";

export async function requireAuth(req: Request, res: Response, next: NextFunction) {
  const session = await auth.api.getSession({ headers: req.headers as Headers });
  if (!session) {
    return res.status(401).json({ status: "error", code: "UNAUTHORIZED" });
  }
  (req as Request & { user: typeof session.user }).user = session.user;
  next();
}

Apply it to any route:

import { requireAuth } from "../middleware/require-auth";

router.get("/profile", requireAuth, async (req, res) => {
  res.json({ user: (req as any).user });
});

JWT — protect a route with token verification:

The generated src/lib/auth.ts exports a verifyAccessToken helper. Use it in a middleware:

import { verifyAccessToken } from "../lib/auth";
import type { Request, Response, NextFunction } from "express";

export function requireAuth(req: Request, res: Response, next: NextFunction) {
  const header = req.headers.authorization;
  if (!header?.startsWith("Bearer ")) {
    return res.status(401).json({ status: "error", code: "UNAUTHORIZED" });
  }
  try {
    const payload = verifyAccessToken(header.slice(7));
    (req as any).user = payload;
    next();
  } catch {
    res.status(401).json({ status: "error", code: "TOKEN_EXPIRED" });
  }
}

Token Refresh Flow (JWT)

Access tokens expire quickly (default 15m). Use the refresh endpoint to get a new one without re-authenticating:

curl -X POST http://localhost:3000/api/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{ "refreshToken": "<your-refresh-token>" }'

Response:

{
  "accessToken": "<new-access-token>",
  "refreshToken": "<new-refresh-token>"
}

Refresh tokens are rotated on each use — the old token is invalidated immediately. Store the latest refresh token securely (httpOnly cookie or secure local storage).


Troubleshooting

BETTER_AUTH_SECRET must be at least 32 characters Generate a valid secret:

openssl rand -base64 32

Cannot find module 'better-auth' The auth module auto-installs dependencies, but if install failed mid-run:

npm install better-auth

JWT TokenExpiredError on every request Your JWT_ACCESS_EXPIRES_IN may be too short or your system clock is out of sync. Verify:

node -e "console.log(new Date().toISOString())"

Database migration fails after adding auth Auth generates a schema file at src/db/schema/auth.ts. Run migrations after adding the module:

npx drizzle-kit generate
npx drizzle-kit migrate

Session not persisting with Better Auth Ensure your frontend sends credentials with requests:

fetch('/api/auth/get-session', { credentials: 'include' });

On this page