Customisation

Common customisation tasks - adjusting route guards, adding OAuth providers, tweaking limits, using the role system, and theming.

Changing route protection

Edit the PUBLIC_ROUTES and AUTH_ROUTES arrays in proxy.ts:

proxy.ts
// Unauthenticated users can access these routes
const PUBLIC_ROUTES = [
  '/',
  '/login',
  '/register',
  '/forgot-password',
  '/reset-password',
  '/verify-email',
  '/about',          // add your public pages here
  '/pricing',
]

// Authenticated users are redirected to /dashboard from these routes
const AUTH_ROUTES = ['/login', '/register']

Adding auth providers

NextAuth v5 supports many OAuth providers. To add GitHub:

  1. Create a GitHub OAuth App at github.com/settings/developers
  2. Set the callback URL to: http://localhost:3000/api/auth/callback/github
  3. Add AUTH_GITHUB_ID and AUTH_GITHUB_SECRET to .env.local
lib/auth.ts
import GitHub from 'next-auth/providers/github'

providers: [
  Credentials({ ... }),
  Google({ ... }),
  GitHub({
    clientId: process.env.AUTH_GITHUB_ID,
    clientSecret: process.env.AUTH_GITHUB_SECRET,
  }),
]

The same JWT and session callbacks will attach id, role, and emailVerified to the token for all providers.

Changing OTP expiry

lib/tokens.ts
// Change this value - used everywhere OTPs are created
export const OTP_EXPIRY_MINUTES = 10

This constant is imported by every action that creates an OTP. Changing it updates expiry across all flows - verification and password reset.

Changing rate limits

lib/ratelimit.ts
// Auth routes (login, register) - default: 5 per minute
export const authRatelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, '1 m'),  // 10 per minute
  prefix: 'nextforge:auth',
})

// OTP routes (send, resend) - default: 3 per 10 minutes
export const otpRatelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(2, '15 m'),  // 2 per 15 minutes
  prefix: 'nextforge:otp',
})

Using roles

The role field is included on every user, propagated through the JWT, and available on the session. Default value: 'user'.

Checking roles in a server action:

export async function adminOnlyAction(): Promise<ActionState> {
  const session = await auth()
  if (!session) {
    return { success: false, error: 'Not authenticated.' }
  }
  if (session.user.role !== 'admin') {
    return { success: false, error: 'Forbidden.' }
  }
  // ... admin logic
}

Checking roles in a server component:

app/admin/page.tsx
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'

export default async function AdminPage() {
  const session = await auth()
  if (!session || session.user.role !== 'admin') {
    redirect('/')
  }
  return <AdminDashboard />
}

To promote a user to admin, update the role field directly in your database. You can build an admin action for this or do it manually via your database dashboard.

Changing the theme

All colors are defined in the @themeblock in app/globals.css. Change the values to update the entire application's color palette:

app/globals.css
@theme {
  /* Change primary color - affects buttons, links, focus rings, active states */
  --color-primary: #7c3aed;          /* purple instead of blue */
  --color-primary-hover: #6d28d9;
  --color-primary-foreground: #ffffff;

  /* Change background colors for a lighter theme */
  --color-background: #ffffff;
  --color-background-card: #f8fafc;
  --color-foreground: #0f172a;
}

No component files need to be touched - every component reads from these variables.

Adding new pages

To add a new protected page:

  1. Create app/your-page/page.tsx as a server component
  2. Add the session check at the top:
app/your-page/page.tsx
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'

export default async function YourPage() {
  const session = await auth()
  if (!session) redirect('/login')

  return (
    <main>
      <h1>Welcome, {session.user.name}</h1>
    </main>
  )
}
  1. If the page should be inaccessible to logged-out users, add it to the protected path check in proxy.ts - or let the redirect() handle it (redirect() is sufficient; proxy.ts is optional for non-auth-route pages)