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 via the in-process
kya-os-engine - Enforce or observe (detect-only) behavior
- Verdict observability through the
onResultcallback - Optional event storage with Redis (production-ready)
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 Checkpoint account with a project created
- A Next.js 13+ or Express 4+ application
- Node.js 18+
- (Optional) Redis for production event storage
Time Estimate
20-25 minutes
Steps
Install the Package
npm install @kya-os/checkpoint-nextjsnpm install @kya-os/checkpoint-expressConfigure Environment Variables
- Go to your Checkpoint dashboard
- Select your project
- Go to Settings → API Keys
- Create or copy your API key
# .env.local
CHECKPOINT_API_KEY=sk_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6
CHECKPOINT_PROJECT_ID=acme-corp-x7a8b9
# For production event storage (Upstash Redis)
REDIS_URL=https://your-db.upstash.io
REDIS_TOKEN=your-upstash-tokenSet Up Basic Enforcement
Create middleware.ts in your project root. This example uses the in-process engine (withCheckpoint):
// middleware.ts
import { withCheckpoint } from '@kya-os/checkpoint-nextjs';
export default withCheckpoint({
tenantHost: 'your.tenant.example',
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 }.
// app.ts
import express from 'express';
import { withCheckpoint } from '@kya-os/checkpoint-express';
const app = express();
app.use(express.json()); // required for MCP-I envelope parsing
app.use(
withCheckpoint({
tenantHost: 'your.tenant.example',
apiKey: process.env.CHECKPOINT_API_KEY, // optional — enables dashboard reporting
// enforcementMode defaults to 'enforce'
})
);
app.get('/', (_req, res) => {
res.json({ message: 'Only humans allowed!' });
});
app.listen(3000);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 { withCheckpoint } from '@kya-os/checkpoint-nextjs';
export default withCheckpoint({
tenantHost: 'your.tenant.example',
apiKey: process.env.CHECKPOINT_API_KEY,
enforcementMode: 'observe', // classify + report, never block
});// app.ts
app.use(
withCheckpoint({
tenantHost: 'your.tenant.example',
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 { withCheckpoint } from '@kya-os/checkpoint-nextjs';
export default withCheckpoint({
tenantHost: 'your.tenant.example',
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,
})
);
},
});// app.ts
app.use(
withCheckpoint({
tenantHost: 'your.tenant.example',
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.path,
})
);
},
})
);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.
Add Production Event Storage (Express)
For production, persist detection events to Redis. Construct a storage adapter with the async createStorageAdapter factory and record events from onResult:
// app.ts
import express from 'express';
import { randomUUID } from 'node:crypto';
import { withCheckpoint, createStorageAdapter } from '@kya-os/checkpoint-express';
const app = express();
app.use(express.json());
const storage = await createStorageAdapter({
type: 'redis',
ttl: 86400, // 24 hours
redis: {
url: process.env.REDIS_URL!,
token: process.env.REDIS_TOKEN!,
},
});
app.use(
withCheckpoint({
tenantHost: 'your.tenant.example',
apiKey: process.env.CHECKPOINT_API_KEY,
onResult: async (result, req) => {
await storage.storeEvent({
eventId: randomUUID(),
sessionId: req.headers['x-request-id']?.toString() ?? randomUUID(),
timestamp: new Date().toISOString(),
agentType: result.detectionDetail.detectionClass.type,
agentName: result.detectionDetail.detectedAgent?.name ?? 'unknown',
confidence: result.detectionDetail.confidence,
path: req.path,
method: req.method,
userAgent: req.headers['user-agent'],
detectionReasons: result.detectionDetail.reasons,
verificationMethod: result.detectionDetail.verificationMethod,
});
},
})
);
app.listen(3000);createStorageAdapter({ type: 'memory' }) (the default) is fine for development but resets on
restart and doesn't share across instances. Use Redis for production.
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:
// app/api/sensitive/route.ts
import { withCheckpointApi } from '@kya-os/checkpoint-nextjs';
export const POST = withCheckpointApi(async (req) => {
// Only executes if not blocked
return Response.json({ secret: 'data' });
});// app.ts
import express from 'express';
import { withCheckpoint } from '@kya-os/checkpoint-express';
const app = express();
app.use(express.json());
// Detect-only middleware for public routes
const detectOnly = withCheckpoint({
tenantHost: 'your.tenant.example',
enforcementMode: 'observe',
});
// Enforcing middleware for sensitive routes
const enforce = withCheckpoint({
tenantHost: 'your.tenant.example',
apiKey: process.env.CHECKPOINT_API_KEY,
});
// Public routes — detect only
app.get('/', detectOnly, (_req, res) => {
res.json({ message: 'Welcome!' });
});
// Sensitive routes — enforce
app.use('/api', enforce);
app.get('/api/data', (_req, res) => {
res.json({ sensitive: 'data' });
});
// Payment routes — enforce
app.use('/checkout', enforce);
app.listen(3000);Test Your Enforcement
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 (compatible; GPTBot/1.0; +https://openai.com/gptbot)" \
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
| Symptom | Cause | Fix |
|---|---|---|
| Humans blocked | Policy too aggressive | Review your policies / composed Cedar |
| Internal traffic blocked | Internal services flagged | Exclude internal routes from the matcher / mount point |
| Can't tell what's caught | Blocking before you've observed | Run 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.
- Persist events off the hot path —
onResulterrors are swallowed, but heavy synchronous work still adds latency; keep storage writes async.
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 persist detection events to Redis via a storage adapter
- How to protect specific routes instead of global enforcement
Next Steps
| Goal | Next Cookbook |
|---|---|
| DNS-level enforcement | Gateway Setup |
| Configure policies | Policy Configuration |
| Authorize agents | Govern Overview |