Keys & security
Which key belongs in which runtime, and what the SDK refuses to let you do.
Two key kinds, one rule: an embed key is public and belongs in the browser; an integration key is a workspace secret and belongs only on a server.
| Runtime | Key kind | Prefix | Env var | Rule |
|---|---|---|---|---|
| Browser | Embed key | embed_test_… / embed_live_… | PUBLIC_OPRAG_EMBED_KEY | Ships in the bundle. Locked to the origins you allowlist on the project. |
| Server | Integration key | sk_test_… / sk_live_… | OPRAG_SECRET_KEY | Read from the process environment. Never sent to, or through, a browser. |
The SDK enforces it
createClient and mountWidget both refuse an sk_* key when they detect a browser, throwing OpragConfigError before any request goes out. This is a guard rail, not a security boundary — the boundary is that you never put the key there.
import { createClient, OpragConfigError } from "@oprag/sdk";
try {
createClient({ apiUrl: "https://api.dev.oprag.ai", apiKey: "sk_live_..." }); // in the browser
} catch (err) {
if (err instanceof OpragConfigError) {
// Use an embed key here, and keep sk_* behind your own API.
}
throw err;
} Embed keys need an Origin
An embed key request with no Origin header is refused with 403 — there is no server-side fallback. That is deliberate: it means a leaked embed key cannot be replayed with curl. Server-to-server calls use an integration key instead.
The origin allowlist is per project, and an empty allowlist rejects everything. Full rules are on CORS & origins.
Naming that carries the boundary
# Ships to the browser. Public by construction.
PUBLIC_OPRAG_EMBED_KEY=embed_live_...
# Server only. Never referenced from client code.
OPRAG_SECRET_KEY=sk_live_... Bundlers expose variables by prefix. Naming the browser key ${KEY_ENV_VARS.browser} and the secret ${KEY_ENV_VARS.server} means the build tooling itself refuses to leak the secret, and a reviewer can see a mistake without knowing the values.
Deciding at runtime
import { createClient, isBrowser } from "@oprag/sdk";
// `isBrowser()` treats a Node process with a DOM shim as *not* a browser,
// because it checks process.versions.node as well as window.
const oprag = createClient({
apiUrl: "https://api.dev.oprag.ai",
apiKey: isBrowser()
? import.meta.env.PUBLIC_OPRAG_EMBED_KEY
: process.env.OPRAG_SECRET_KEY!,
}); Ready to ship?
Get started free