Next.js
npm install next x511The Next.js adapter targets the App Router (app/ directory). Two files are needed: a shared module that creates the x511 instance, and a catch-all route that exposes verify over HTTP.
Centralized configuration
// lib/x511.ts
import { x511, self, zkpassport } from 'x511/next'
export const { verify, verified } = x511({
domain: 'https://your-app.example.com',
basePath: '/x511',
dev: true,
disclosures: {
minAge: 18,
nationality: { included: ['HUN', 'POL', 'SVK', 'CZE', 'USA'] },
},
mode: { type: 'session', ttl: 1800 },
providers: [
self({ scope: 'self-playground', appName: 'Next.js Demo' }),
zkpassport(),
],
})Defining the instance in a dedicated module (e.g. lib/x511.ts) keeps configuration out of route files and ensures every route imports the same verify/verified pair.
Mount verify on a catch-all route
// app/x511/[...path]/route.ts
import { verify } from '@/lib/x511'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
export const fetchCache = 'force-no-store'
export const revalidate = 0
export const GET = (req: Request) => verify(req)
export const POST = (req: Request) => verify(req)The directory name (x511) must match the basePath you configured. Both GET (for /sse) and POST (for the provider routes and /claim) need to route through verify. The runtime/dynamic/fetchCache/revalidate exports keep Next.js from caching the SSE stream or the claim exchange.
Protect a route
// app/protected/route.ts
import { verified } from '@/lib/x511'
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
export const fetchCache = 'force-no-store'
export const revalidate = 0
export const GET = verified(
async (_req, identity) => new Response(identity.uniqueId ?? 'anon'),
)verified is a higher-order function: pass a handler (request, identity) => Response | Promise<Response> and it returns a route handler that runs only when the cookie is valid. When the cookie is missing or expired it returns the 511 HTML page directly.
The identity argument is { uniqueId?: string }. The shape is exported as VerifiedIdentity from x511/next.
Tips
- Keep
verifyandverifiedin one module — Next.js may evaluate route files in separate execution contexts. Importing both from a single shared module ensures they reference the same internal session state when using the in-memory adapter. - Use a real session adapter in production — multiple Next.js instances (or the dev/prod boundary) cannot share the in-memory adapter. See Session storage for a Redis sketch.
- Disable caching on protected routes — the
dynamic,fetchCache, andrevalidateexports are required to prevent Next from caching the verification flow or the protected response.
Mark @zkpassport/sdk as an external server package
If you use the zkpassport provider on Next.js, add @zkpassport/sdk to serverExternalPackages in next.config.mjs. The ZKPassport SDK depends on @aztec/bb.js, which loads a .wasm.gz file from disk using import.meta.url. When Next.js bundles it into the server output, that path no longer resolves and proof verification fails with ENOENT: no such file or directory ... barretenberg-threads.wasm.gz. Marking the package as external keeps it as a runtime require() so the WASM resolves correctly.
// next.config.mjs
const nextConfig = {
serverExternalPackages: ['@zkpassport/sdk'],
}
export default nextConfig