Cookbooks

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 onResult callback
  • 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-nextjs

Configure 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-x7a8b9

Set 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

In Next.js 16 this file convention was renamed. Name the file 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 data

Test 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

SymptomCauseFix
Humans blockedPolicy too aggressiveReview your policies / composed Cedar
Internal traffic blockedInternal services flaggedExclude internal routes from the matcher / mount point
Can't tell what's caughtBlocking before you've observedRun enforcementMode: 'observe' and read onResult first

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: onResult errors are swallowed, but onResult is 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 onResult callback
  • How to record verdicts in your own store from onResult
  • How to protect specific routes instead of global enforcement

Next Steps

GoalNext Cookbook
DNS-level enforcementGateway Setup
Configure policiesPolicy Configuration
Authorize agentsGovern Overview