Skip to content

Next.js

bash
npm install next x511

The 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

ts
// 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

ts
// 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

ts
// 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 verify and verified in 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, and revalidate exports 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.

js
// next.config.mjs
const nextConfig = {
  serverExternalPackages: ['@zkpassport/sdk'],
}

export default nextConfig