Checkpoint Docs
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 via the in-process kya-os-engine
  • Enforce or observe (detect-only) behavior
  • Verdict observability through the onResult callback
  • 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-nextjs
npm install @kya-os/checkpoint-express

Configure Environment Variables

  1. Go to your Checkpoint dashboard
  2. Select your project
  3. Go to Settings → API Keys
  4. 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-token

Set 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 data

Test 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

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.
  • Persist events off the hot pathonResult errors 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 onResult callback
  • How to persist detection events to Redis via a storage adapter
  • How to protect specific routes instead of global enforcement

Next Steps

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