checkrd

Next.js

Add Checkrd policy enforcement to Next.js Route Handlers and Server Actions.

Next.js

Checkrd ships first-class Next.js helpers for both the App Router (Route Handlers) and Server Actions, with the right initialization shape for the Edge Runtime and Node runtime simultaneously.

Install

bash
npm install checkrd

Initialize once

Next.js bundles each route file separately. initCheckrd caches the engine across requests within an isolate so the WASM only compiles once. It returns a promise resolving to { fetch, isNode }; pass that fetch to any vendor SDK you call.

typescript
// app/lib/checkrd.ts
import { initCheckrd } from "checkrd/next";

export const checkrd = initCheckrd({
  agentId: "web",
  apiKey: process.env.CHECKRD_API_KEY!,
});

initCheckrd is idempotent across hot reloads and works in both Node and Edge runtimes (it uses initAsync under the hood, which fetches your published policy from the control plane and installs it before resolving). Tests can call resetCheckrdNext() between cases.

Where does the policy come from?

No policy: argument — the SDK fetches the agent's currently-published DSSE-signed bundle from GET /v1/agents/:id/control/state synchronously at boot. Edit the policy in the dashboard and updates stream to running isolates over SSE.

Route Handlers (App Router)

checkrdRoute(handler, options) wraps the handler. The handler receives { request, fetch }; pass fetch to OpenAI / Anthropic / any vendor SDK that accepts it:

typescript
// app/api/chat/route.ts
import OpenAI from "openai";
import { checkrdRoute } from "checkrd/next";

export const POST = checkrdRoute(
  async ({ request, fetch }) => {
    const { messages } = await request.json();
    const openai = new OpenAI({ fetch });
    const result = await openai.chat.completions.create({
      model: "gpt-4o-mini",
      messages,
    });
    return Response.json(result);
  },
  { agentId: "web", apiKey: process.env.CHECKRD_API_KEY! },
);

On deny, checkrdRoute returns a 403 with the error envelope:

json
{
  "error": {
    "type": "policy_denied",
    "message": "deny rule 'block-pii' fired",
    "request_id": "...",
    "dashboard_url": "https://app.checkrd.io/events/..."
  }
}

Server Actions

checkrdAction(handler, options) wraps a Server Action. The handler receives { fetch } as its first argument; the action's own arguments follow:

typescript
// app/actions.ts
"use server";
import { checkrdAction } from "checkrd/next";

export const summarize = checkrdAction(
  async ({ fetch }, input: string) => {
    // ... call a vendor SDK with `fetch` here ...
    return { summary: input.slice(0, 100) };
  },
  { agentId: "web", apiKey: process.env.CHECKRD_API_KEY! },
);

What gets enforced

HelperSynthetic URLBody
checkrdRoute{request.method} {request.url}The request body (JSON or raw)
checkrdActionnext-action://{actionName}The action arguments

You write rules against these the same as any other Checkrd rule.

yaml
default: deny

rules:
  - name: allow-public-chat
    allow:
      method: [POST]
      url: "**/api/chat"

  - name: allow-summarize-action
    allow:
      url: "next-action://summarize"

Pages Router (legacy)

The same helpers work for Pages Router API routes; wrap your default export with checkrdRoute. Recommended only for existing Pages Router apps; new projects should use App Router.

Edge Runtime

Both helpers detect the runtime automatically. In Edge, initCheckrd loads the WASM via fetch + WebAssembly.compile. No node:* imports happen at module load; the bundle is edge-clean. The tests/edge_runtime.test.ts regression test in the repo locks this in.

Caveats

  • initCheckrd is module-scoped. Don't call it inside the handler body; the engine should be reused across requests for sub-millisecond evaluation.
  • Environment variables. In Edge Runtime, only process.env.CHECKRD_API_KEY and friends declared in next.config.ts are visible. Pass them explicitly when needed.
  • Server Actions arguments are not always JSON-serializable. The adapter uses JSON.stringify with a fallback to String(value); complex objects (Dates, Buffers, FormData) lose detail in body matchers.