Browser usage
Why the JavaScript SDK refuses to run in browsers by default, when the opt-in is appropriate, and what server-side patterns to use instead.
Browser usage
The JavaScript SDK refuses to initialize in a browser by default. This page explains why, when the explicit opt-in is appropriate, and the two server-side patterns that cover almost every legitimate use case.
Why the default is "no"
The SDK holds two pieces of process-memory state that don't belong on end-user devices:
- The Checkrd API key (
ck_live_...). With this key, anyone can read your decision history, publish policies, and rotate other keys. - The Ed25519 private key that signs every telemetry batch. With this key, an attacker can forge telemetry that appears to come from your agents.
Bundling either of these into JavaScript shipped to a browser ships
them to every visitor of the page. Even if you obfuscate, the keys
are extractable in seconds — dangerouslyAllowBrowser: true in the
OpenAI and Anthropic SDKs follows the same logic and the same
naming.
So the default behavior of new Checkrd({ apiKey }) in a browser is
to throw CheckrdInitError with code browser_use_detected before
any work is done.
The two patterns we recommend instead
1. Server-side proxy (recommended)
Put a thin proxy in front of your model provider — Next.js Route Handler, Hono server, Cloudflare Worker, Lambda function, anything that can hold a secret. The browser calls your proxy; the proxy holds the Checkrd-instrumented client and the upstream API key.
// app/api/chat/route.ts (Next.js)
import { Checkrd } from "checkrd";
import OpenAI from "openai";
const checkrd = new Checkrd({
agentId: process.env.CHECKRD_AGENT_ID!,
apiKey: process.env.CHECKRD_API_KEY!,
});
// `instrumentOpenAI()` monkey-patches the OpenAI module so every
// `new OpenAI()` after this point uses a Checkrd-wrapped fetch.
checkrd.instrumentOpenAI();
const openai = new OpenAI();
export async function POST(req: Request) {
const { messages } = await req.json();
const completion = await openai.chat.completions.create({
model: "gpt-5.2",
messages,
});
return Response.json(completion);
}This is the pattern Vercel, Cloudflare, and the model-provider docs all recommend independently of Checkrd. It also gives you per-user auth (whatever your app uses), per-user rate limiting, and a cleaner audit story — every Checkrd decision is tagged with the server's agent ID, not the browser's.
2. Edge function gateway
Same shape as #1, run at the edge. wrapMcpClient, the Cloudflare
Workers integration (withCheckrd), and the Vercel AI SDK middleware
(createCheckrdMiddleware) all assume this topology. See
Cloudflare Workers,
Hono, and
Next.js for full code.
Edge runtimes don't change the security argument — they're just a faster, cheaper proxy.
When the opt-in is appropriate
dangerouslyAllowBrowser: true exists for a small set of legitimate
cases:
- Internal tools where every browser session is signed in to your own SSO, and the "API key" is per-user (issued via your login flow) rather than a shared service key.
- Dev tooling and storybooks where you're running against a
test control plane (
api-staging.checkrd.io) with ack_test_key. - Trusted-installer desktop apps (Tauri, Electron) where the "browser" is actually a single-user shell with no untrusted content.
const client = new Checkrd({
apiKey: userScopedKey,
dangerouslyAllowBrowser: true,
});The flag silences the init error. It does not change the security posture: anything the SDK does, the page can also do.
If you're tempted to use it for a production user-facing app, reconsider. The proxy pattern above is two files of code and removes the entire class of risk.
Test-mode keys
If you're prototyping against dangerouslyAllowBrowser: true, use a
ck_test_* API key. Test keys can't publish policies, can't rotate
production keys, and can only post telemetry to a sandbox org. They
exist for exactly this case.
Generate one in the dashboard under API Keys → Create test key.
What the SDK detects
The browser check looks for typeof window !== "undefined" plus the
absence of node:-runtime markers. Edge runtimes (Cloudflare
Workers, Vercel Edge, Deno Deploy) do not trip the check — they
have no DOM. Bun and Deno running server code do not trip the check
either.
See also
- Error handling —
CheckrdInitError.codereference - Cloudflare Workers integration — edge proxy template
- Next.js integration — server-side proxy in App Router
- Identity & signing — why the private key matters