Enforce: Middleware Enforcement
Block AI agents in your application code with Next.js or Express middleware
Goal
Add code-level AI agent enforcement to your Next.js or Express application. By the end of this cookbook, you'll have:
- Server-side detection on every request, with Vouched KYA's detection engine running in-process
- Enforce or observe (detect-only) behavior
- Verdict observability through the
onResultcallback - Optional verdict records in your own store (Express)
Best for: Applications where you want the detection engine running in-process with fine-grained observability, custom response handling, or integration with existing authentication.
Prerequisites
- A Vouched KYA account with a project created
- A Next.js 13+ or Express 4+ application
- Node.js 20.19+ or 22.12+
Time Estimate
20-25 minutes
Steps
Install the Package
npm install @kya-os/checkpoint-nextjsConfigure Environment Variables
Find your project's API key and Project ID on Installations: see Credentials for the exact click-path and key format.
# .env.local
CHECKPOINT_API_KEY=sk_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6
CHECKPOINT_PROJECT_ID=acme-corp-x7a8b9Set Up Basic Enforcement
For current NextRequest objects (Next.js 15+), client-address evidence requires Vercel's managed runtime; self-hosted proxy and Cloudflare profiles return no address. Express behind a proxy requires an explicit proxy profile and a peer allowlist. Its default policy withholds a socket that Express regards as a proxy. See client-IP deployment guidance.
Create middleware.ts in your project root. This example uses the in-process engine (withCheckpoint) on the Edge runtime; see Next.js runtimes and bundlers for webpack and the Node.js runtime:
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 Vouched KYA setup below is the same either way, except that withCheckpoint on the Edge runtime also takes engineWasmModule. One caveat: proxy.ts runs on the Node.js runtime only, so if you want Vouched KYA on the Edge runtime (lowest latency), keep the file as middleware.ts. On Next.js 15 and earlier, use middleware.ts.// middleware.ts
import engineWasmModule from '@kya-os/checkpoint-nextjs/engine-wasm';
import { withCheckpoint } from '@kya-os/checkpoint-nextjs';
export default withCheckpoint({
// Managed Vercel only; current self-hosted Next has no authenticated client-IP source.
clientIpPolicy: { platform: 'vercel' },
engineWasmModule, // the engine's compiled WebAssembly, required on the Edge runtime
apiKey: process.env.CHECKPOINT_API_KEY, // optional: enables dashboard reporting
// enforcementMode defaults to 'enforce': detected agents are blocked
});
export const config = {
matcher: [
// Match all paths except static assets
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};Prefer the SaaS gateway (no local WASM)? Swap the import for withCheckpointApi from @kya-os/checkpoint-nextjs/api-middleware and pass { apiKey }.
Run Detect-Only First
Before you start blocking, run in observe mode. Every request passes through, but the engine classifies each one and stamps X-Checkpoint-Would-Have-Been headers so you can see what enforcement would have done. Detections still report to the dashboard when apiKey is set.
// middleware.ts
import engineWasmModule from '@kya-os/checkpoint-nextjs/engine-wasm';
import { withCheckpoint } from '@kya-os/checkpoint-nextjs';
export default withCheckpoint({
// Managed Vercel only; current self-hosted Next has no authenticated client-IP source.
clientIpPolicy: { platform: 'vercel' },
engineWasmModule, // the engine's compiled WebAssembly, required on the Edge runtime
apiKey: process.env.CHECKPOINT_API_KEY,
enforcementMode: 'observe', // classify + report, never block
});When you're confident in the classifications, drop enforcementMode (it defaults to 'enforce') to start blocking.
Observe the Verdict
withCheckpoint attaches nothing to req: read the engine's verdict through the onResult(result, req) callback. It fires after every verification (permit or block); errors thrown inside it are swallowed so observability can't break the response.
// middleware.ts
import engineWasmModule from '@kya-os/checkpoint-nextjs/engine-wasm';
import { withCheckpoint } from '@kya-os/checkpoint-nextjs';
export default withCheckpoint({
// Managed Vercel only; current self-hosted Next has no authenticated client-IP source.
clientIpPolicy: { platform: 'vercel' },
engineWasmModule, // the engine's compiled WebAssembly, required on the Edge runtime
apiKey: process.env.CHECKPOINT_API_KEY,
onResult: (result, req) => {
console.log(
JSON.stringify({
event: 'checkpoint_verdict',
verdict: result.decision.kind,
class: result.detectionDetail.detectionClass.type,
agent: result.detectionDetail.detectedAgent?.name,
confidence: result.detectionDetail.confidence,
path: req.nextUrl.pathname,
})
);
},
});The verdict comes from the engine plus any composed Cedar policy you
deploy, not from a per-request onBlock callback. Use onResult to observe and side-effect (log,
alert, persist); use policies to change what gets blocked.
Record Verdicts in Your Own Store (Express)
With apiKey set, every verdict already reaches your dashboard. To also keep verdicts in your own logs or database, write them from onResult:
// app.ts
import express from 'express';
import { withCheckpoint } from '@kya-os/checkpoint-express';
const app = express();
app.use(express.json());
app.use(
withCheckpoint({
apiKey: process.env.CHECKPOINT_API_KEY,
onResult: async (result, req) => {
await events.insert({
decision: result.decision.kind,
detectionClass: result.detectionDetail.detectionClass.type,
agent: result.detectionDetail.detectedAgent?.name ?? null,
confidence: result.detectionDetail.confidence,
path: req.path,
method: req.method,
at: new Date().toISOString(),
});
},
})
);
app.listen(3000);events.insert stands for your own store's client. See Express Observability for reading the verdict in a route handler.
Protect Specific Routes
Instead of global middleware, protect only specific routes:
Match specific paths in the matcher:
// middleware.ts
export const config = {
matcher: [
'/api/:path*', // All API routes
'/dashboard/:path*', // Dashboard routes
'/checkout/:path*', // Checkout flow
],
};Per-route protection with the SaaS gateway: withCheckpointApi is middleware too, so scope it with includePaths (or the matcher above) instead of wrapping a route handler:
// middleware.ts
import { withCheckpointApi } from '@kya-os/checkpoint-nextjs/api-middleware';
export default withCheckpointApi({
apiKey: process.env.CHECKPOINT_API_KEY,
includePaths: ['/api/sensitive'], // the route and everything under it
});Verify It's Working
Test as a human:
curl http://localhost:3000/api/data
# Should return 200 OK with dataTest as an AI agent (should be blocked):
curl -H "User-Agent: Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot" \
http://localhost:3000/api/data
# Should return a 4xx block response (JSON-API clients) or a redirect (HTML clients)Check your dashboard → Analytics to see the detected requests.
Troubleshooting
All Requests Being Blocked
Blocking Not Working
- Check
enforcementMode: must be'enforce'(the default), not'observe'. - Check the matcher / mount point: the route must be covered by the middleware.
- Check your policy: enforcement rules come from your deployed policy, not a per-request callback.
Performance Issues
- Narrow the matcher: only protect necessary routes.
- Keep slow writes off the request path:
onResulterrors are swallowed, butonResultis awaited before the response, so a slow write delays the request; hand slow writes to a queue.
What You Learned
- How to set up the in-process enforcement engine in Next.js and Express with
withCheckpoint - How to run detect-only with
enforcementMode: 'observe'before enforcing - How to observe verdicts with the
onResultcallback - How to record verdicts in your own store from
onResult - How to protect specific routes instead of global enforcement