next-api-layerNext API Layer
DocumentationAPI ReferenceExamples
next-api-layerNext API Layer

Production-grade API layer for Next.js with external JWT backends.

Documentation

  • Introduction
  • Installation
  • Quick Start
  • API Reference

Resources

  • Examples
  • Proxy
  • API Client
  • AuthProvider

Community

  • GitHub
  • Issues
  • Discussions
  • Contact

© 2026 Next API Layer. All rights reserved.

Created by
Documentation

Getting Started

  • Introduction
  • Installation
  • Quick Start

Core Concepts

  • How It Works
  • Token Management
  • Guest Tokens

Configuration

  • Auth Proxy
  • Proxy Handler
  • API Client
  • Security
  • i18n Integration

Client Side

  • AuthProvider
  • useAuth Hook

API Reference

  • API Reference
  • Types

Examples

  • Examples
  • Authentication Patterns
  • Role-Based Access
  • API Routes
  • Forms
  • Data Fetching
Changelog

Getting Started

  • Introduction
  • Installation
  • Quick Start

Core Concepts

  • How It Works
  • Token Management
  • Guest Tokens

Configuration

  • Auth Proxy
  • Proxy Handler
  • API Client
  • Security
  • i18n Integration

Client Side

  • AuthProvider
  • useAuth Hook

API Reference

  • API Reference
  • Types

Examples

  • Examples
  • Authentication Patterns
  • Role-Based Access
  • API Routes
  • Forms
  • Data Fetching
Changelog

Token Management

next-api-layer handles JWT token management automatically, including validation, refresh, single-flight coalescing, and optional local verification.

Overview#

The library manages these token operations:

  1. Validation: Checking if a token is valid (backend per-request, or local JWT verification)
  2. Refresh: Automatically refreshing expired tokens, coalescing concurrent refreshes
  3. Reuse detection: Classifying refresh failures and failing closed on token theft (RFC 9700)

Token Validation#

In the default backend mode, every request triggers token validation through your backend's validate endpoint. Switch to validate.mode: 'local' to verify the JWT in the proxy instead and skip that call — see Local Validation (Stateless) below.

TypeScript
Loading...

Expected Response Format#

Your backend's /auth/me endpoint should return:

JSON
Loading...

Custom Response Parsing#

If your backend uses a different format, use responseMappers:

TypeScript
Loading...

Automatic Token Refresh#

When a token is invalid or expired, the library automatically attempts to refresh it:

TypeScript
Loading...

Refresh Flow#

  1. Token validation fails (expired/invalid)
  2. Library calls /auth/refresh with current token
  3. If successful, updates cookie with new token
  4. Sets x-refreshed-token header for the request
  5. Continues with the original request

Expected Refresh Response#

JSON
Loading...

Custom Refresh Parsing#

TypeScript
Loading...

Concurrent Refresh (Single-Flight)#

When several requests arrive at once with the same expired token (navigation + SWR polling + RSC fetches), the proxy coalesces their refreshes into a single auth/refresh call and shares the result:

Loading...

This matters for backends that rotate jti server-side: without coalescing, the second refresh would replay a rotated token and bounce the user to login (or orphan the session). Enabled by default (refresh.singleFlight), effective within a single runtime instance.

Sharing refresh results across instances#

The single-flight map lives in memory, so it only covers one runtime instance. PM2 cluster workers, Passenger, Docker replicas and serverless/edge isolates each keep their own. Supply refresh.store so one instance can reuse a refresh another just performed:

TypeScript
Loading...

Keys are SHA-256 hashes of the old token, never the token itself. Values hold freshly issued tokens for storeTtlMs, so back the store with a secured service and keep the TTL small. Only a refreshed token that passes validation is stored. Stale entries fall through to a normal refresh, and store errors are reported via onError without failing the request.

refresh.store is best-effort, not a distributed mutex — get and set are not atomic, so two instances that miss the store at the same moment still refresh independently. For full coverage, pair either mechanism with idempotent refresh handling on the backend: a rotation grace window, and acceptance of the previous jti for that window, chosen larger than your refresh latency.

Auth API Routes#

By default the proxy skips /api/auth/login, /api/auth/logout, /api/auth/me, /api/auth/refresh and /api/auth/register entirely, so none of the refresh machinery above applies to them.

/api/auth/me is the route AuthProvider polls, and while it is bypassed an expired token simply returns 401 there. Remove it from the list to route it through the normal pipeline:

TypeScript
Loading...

Now an expired token on /api/auth/me triggers a single refresh, the new token is written to the cookie and forwarded downstream via x-refreshed-token, and the route returns 200.

Keep the login and register routes listed — they carry no token yet — and keep the refresh route listed, since validating it would rotate the token before the route itself runs.

Inside afterAuth, skipped requests are marked so you can tell them apart from genuinely anonymous ones:

TypeScript
Loading...

Proactive Refresh#

By default, refresh is reactive — triggered only after a 401. Enable proactive refresh to renew a still-valid token shortly before it expires, avoiding a guaranteed failed roundtrip on the next cycle:

TypeScript
Loading...

Proactive refresh is best-effort: if it fails, the still-valid token keeps working. A detected token reuse remains terminal.

Reuse Detection & Fail-Closed (RFC 9700)#

A failed refresh is classified so you can react to token theft:

reasonMeaningDefault trigger
expiredToken past its lifetime401
revokedServer rejected the token403
reuseA rotated (old) token was replayed409 or body { code: 'token_reuse' }
networkTransport errorfetch threw
unknownAnything else—
TypeScript
Loading...

On reuse — or when access.guestFallbackOnUserRefreshFail: false — the proxy clears every auth cookie and redirects to login (or 401 for API routes), emitting auth:reuse / auth:refresh:fail audit events. It never downgrades to a guest session.

Local Validation (Stateless)#

By default the proxy calls auth/me on every request. Switch to local verification to validate the JWT in-process and remove the per-request backend roundtrip:

TypeScript
Loading...

revalidateInterval preserves revocation / allowlist enforcement: the backend is consulted at most once every N seconds (tracked via the non-sensitive __nal_rv cookie). Use validate.verify for RS256 / JWKS.

Dual-Token Mode#

For the OAuth2 access/refresh split — a short-lived access token plus a separate, long-lived refresh token — name a refresh cookie:

TypeScript
Loading...

The access token is validated on each request; only the refresh cookie is sent to auth/refresh, which rotates both tokens. Even if the access token leaks in a log or proxy, it is short-lived.

Token Types#

The library supports different token types:

TypeScript
Loading...

Type Checking#

TypeScript
Loading...

Guest vs User Tokens#

FeatureGuest TokenUser Token
Created automaticallyYesNo (login required)
Cookie nameguestTokenuserToken
Typical TTL1 hour7 days
Can access protected routesNoYes

Cookie Configuration#

Control how tokens are stored:

TypeScript
Loading...

Security Best Practices#

  • Always use httpOnly: true to prevent XSS token theft
  • Use secure: true in production for HTTPS-only
  • sameSite: 'lax' provides good CSRF protection while allowing normal navigation
  • Set appropriate maxAge based on your security requirements

Accessing Tokens#

In Route Handlers#

TypeScript
Loading...

With API Client#

The API client handles token retrieval automatically:

TypeScript
Loading...

Token Expiration Handling#

Proxy Level#

The proxy handles expiration automatically:

  1. Detects expired token from validation response
  2. Attempts refresh
  3. If refresh fails, clears cookies and redirects to login (for protected routes)

Client Level#

The AuthProvider keeps client state in sync:

TypeScript
Loading...

Debugging#

Enable audit logging to track token operations:

TypeScript
Loading...

This helps identify:

  • Frequent refresh attempts (might indicate short token TTL)
  • Failed validations (might indicate backend issues)
  • Unauthorized access attempts