Next.js Integration
Edge-optimized AI agent detection for Next.js applications
Overview
The Checkpoint Next.js integration provides AI agent detection and enforcement for Next.js applications. With edge runtime optimization and full App Router support, it delivers protection with minimal performance impact.
- Edge Runtime Compatible: Runs at the edge for ultra-low latency
- Enforce or Observe modes: Active enforcement or log-only dry runs
- Route Matching: Fine-grained control over protected paths
- Policy Engine: Local policy evaluation with configurable rules
- TypeScript: Full type definitions included
Prerequisites
- A Checkpoint project: get your Project ID and API key from Installations in the dashboard.
- A Next.js app (App Router or Pages Router).
Installation
npm install @kya-os/checkpoint-nextjsQuick Setup
Next.js 16: middleware.ts โ proxy.ts
proxy.ts and export a proxy function (a default export also works); middleware.ts exporting middleware still works but is deprecated. The Checkpoint setup below is identical either way: only the file name and export name change. One caveat: proxy.ts runs on the Node.js runtime only, so if you want Checkpoint on the Edge runtime (lowest latency), keep the file as middleware.ts. On Next.js 15 and earlier, use middleware.ts.Create Middleware
Create middleware.ts in your project root:
import { withCheckpointApi } from '@kya-os/checkpoint-nextjs/api-middleware';
export default withCheckpointApi({
// Vercel only. Self-hosted: use 'trusted-proxy' with trustedHops set to your proxy count minus one.
clientIpPolicy: { platform: 'vercel' },
apiKey: process.env.CHECKPOINT_API_KEY,
});
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};Add Environment Variables
# .env.local
CHECKPOINT_API_KEY=your_api_key_hereVerify It's Running
Enforcement happens in the middleware, before your pages and route handlers run: blocked or challenged requests never reach your code, so handlers don't need their own detection checks. To confirm the middleware is active, inspect the response headers it stamps:
curl -sI https://your-site.example/ -A "GPTBot/1.0" | grep -i -e '^kya-' -e '^x-checkpoint'See Response Headers below for what to expect in the output.
Local Engine Middleware
The package ships two deployment shapes. withCheckpointApi (above) dispatches to the Checkpoint SaaS gateway over HTTPS. withCheckpoint runs the detection engine in-process (WASM), lowest latency, deterministic verdicts, no per-request network hop:
// middleware.ts
import { withCheckpoint } from '@kya-os/checkpoint-nextjs';
export default withCheckpoint({
// Vercel only. Self-hosted: use 'trusted-proxy' with trustedHops set to your proxy count minus one.
clientIpPolicy: { platform: 'vercel' },
tenantHost: 'your.tenant.example',
apiKey: process.env.CHECKPOINT_API_KEY, // optional: enables dashboard reporting
projectId: process.env.CHECKPOINT_PROJECT_ID, // optional: enforces your deployed Cedar policy
});
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'],
};The legacy createEnhancedAgentShieldMiddleware and withAgentShield exports are deprecated
migration stubs: createEnhancedAgentShieldMiddleware throws at runtime. Use withCheckpoint
(local engine) or withCheckpointApi (SaaS gateway) instead.
See Middleware Enforcement for the full config reference for both shapes.
Read the browser posture outcome
With browser posture configured, your route handlers and pages can read the posture outcome for a request. getCheckpointPostureOutcome verifies the posture token from the request's KYA-Posture headers or cookies with the same verifier and the same posture config withCheckpoint takes, so nothing is forwarded from the middleware and there is no extra secret to manage.
// checkpoint-config.ts
export const browserPosture = { publicKey, keyId, origin };
export const projectId = process.env.CHECKPOINT_PROJECT_ID!;
// middleware.ts
import { withCheckpoint } from '@kya-os/checkpoint-nextjs';
import { browserPosture, projectId } from './checkpoint-config';
export default withCheckpoint({ tenantHost: 'example.com', projectId, browserPosture });// app/api/checkout/route.ts
import { getCheckpointPostureOutcome } from '@kya-os/checkpoint-nextjs';
import { browserPosture, projectId } from '@/checkpoint-config';
export async function POST(request: Request) {
const outcome = await getCheckpointPostureOutcome(request, { browserPosture, projectId });
if (outcome.status === 'rejected') console.warn('posture rejected', outcome.reason);
return Response.json({
posture: outcome.status,
verdict: outcome.posture?.verdict,
});
}The result is { status, reason?, posture? }. status is verified, absent, or rejected, reason names why a token was rejected, and posture holds the verified claims, as a frozen copy, only when the status is verified. status: 'verified' means the posture token's signature and claims are valid. It does not mean the visitor is a person, so check posture.verdict before treating the visitor as human. A verified token that carries the beacon's agent or bot verdict (posture.detection), or discloses automation, also reclassifies a request the engine called human or incomplete data.
The same import works in Node and Edge route handlers and in Edge middleware: the package resolves to its Edge build under the Edge runtime, and that build exports getCheckpointPostureOutcome too. The verifier uses only Web Crypto, so export const runtime = 'edge' needs no other change.
The accessor always checks. It returns verified, absent, or rejected for the request it is given, whether or not the middleware ran for that route. In a server component, pass await headers() instead of a request: getCheckpointPostureOutcome(await headers(), { browserPosture, projectId }). Verification is a single ES256 check, so repeating it in a handler costs little.
The accessor validates browserPosture and projectId with the same check withCheckpoint runs at startup: projectId must be the lowercase project UUID, not the friendly id, and any invalid config throws, so a wrong value fails loudly instead of reporting absent. Pass the same config object the middleware takes, so the two cannot disagree about origins.
Treat absent and rejected alike: both mean the request carries no usable posture evidence (a missing token, a duplicated or ambiguous header, and an invalid token all look like that), so neither should grant anything. Log reason on the server rather than returning it to the client, because it tells a prober which check failed.
Configuration
import { withCheckpointApi } from '@kya-os/checkpoint-nextjs/api-middleware';
export default withCheckpointApi({
// Vercel only. Self-hosted: use 'trusted-proxy' with trustedHops set to your proxy count minus one.
clientIpPolicy: { platform: 'vercel' },
apiKey: process.env.CHECKPOINT_API_KEY, // or set the CHECKPOINT_API_KEY env var
// Optional
onBlock: 'block', // 'block' | 'redirect' | 'challenge', default: dashboard policy
redirectUrl: '/for-ai', // target when onBlock is 'redirect'
useEdge: true, // edge detection (catches non-JS clients), default: true
timeout: 5000, // request timeout in ms
});See Middleware Enforcement for the complete option tables for withCheckpointApi and withCheckpoint.
Environment Variables
# .env.local
CHECKPOINT_API_KEY=your_api_key_hereEnforce vs Observe Mode
On the local-engine shape, start with withCheckpoint({ enforcementMode: 'observe', ... }) to understand your traffic, review the would-have-been decisions in the dashboard, then switch to enforce when ready. On the SaaS shape (withCheckpointApi), enforcement follows your dashboard policy: deploy the policy in observe mode instead.
Response Headers
The SaaS shape (withCheckpointApi) sets KYA-* headers (KYA-Detected, KYA-Confidence, KYA-Agent on allowed agent traffic; KYA-Action / KYA-Reason on enforcement). The local-engine shape (withCheckpoint) sets X-Checkpoint-* headers and a __checkpoint_verdict cookie, adding X-Checkpoint-Would-Have-Been in observe mode.
See Middleware Enforcement for the full header list shared with Express and .NET.
Client-Side Detection
This package is server-side middleware: it does not ship client-side detection hooks. (The legacy useAgentDetection hook was removed along with the AgentDetector class it wrapped.) For client-side detection (conditionally rendering content, tracking agent visits from the browser), use the JavaScript Beacon or the Marketing Pixel alongside the middleware.
Edge Runtime
withCheckpoint runs its engine in-process on the Node.js runtime. On Vercel, use withCheckpointApi: it dispatches to the Checkpoint edge over HTTPS, and route handlers behind it need no detection code of their own. Enforcement has already happened by the time they run:
// app/api/protected/route.ts
export const runtime = 'edge';
export async function GET() {
// Only requests your policy allows ever reach this point.
return new Response('Protected data');
}Content Security Policy
If your site uses CSP headers and you're also using the Pixel, add the Checkpoint domain:
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' https://kya.vouched.id",
"connect-src 'self' https://kya.vouched.id",
].join('; '),
},
],
},
];
},
};Troubleshooting
Middleware not running
- Ensure
proxy.ts(Next.js 16+) ormiddleware.tsis at the project root, or insrc/when the app uses asrc/directory; never insideapp/ - Check the
matcherconfiguration - Verify the file exports a default function
Detection not working
- Verify
CHECKPOINT_API_KEYis set: without it, detections never reach the dashboard - Check for API calls to
kya.vouched.id(SaaS shape) in your server logs - Enable
debug: truefor detailed logs
Performance issues
- Prefer
withCheckpoint(in-process WASM engine): no per-request network hop - Keep
useEdge: trueonwithCheckpointApifor the lower-latency edge path - Scope the
matcherso static assets and health checks bypass the middleware
Next Steps
- Middleware Enforcement: Complete middleware documentation with all features
- Policies: Configure enforcement rules
- Gateway: DNS-based enforcement (no code changes)
- Express Integration: Server-side middleware for Express
- Choose Your Integration: Compare all integration options
