skip to content
docs / reference

Using the proxy / BYOK.

How this site keeps the TypeSafe key on the server, and how to bring your own.

Why a proxy#

A TypeSafe key in client-side JavaScript is a key on the internet. So the studio and every “run it” button on this site never talk to TypeSafe directly: the browser runs the chain (the runtime, the trace, the decisions are all local), and only the HTTP calls to Jev go through one small route handler on this server, /api/jev, which adds the key and forwards them.

  • The key stays server-side. TYPESAFE_API_KEY is read from the server's environment and sent upstream as a bearer token. It never reaches the browser.
  • Bodies are shape-checked first. JSON only, 64 KB max, a model, a state, and 1 to 64 named questions that each have a type. Garbage is rejected before it costs anyone a request.
  • Everything else passes through. TypeSafe's status, body and retry-after come back untouched, so the client's error classes and retries work exactly as they would against the real API. Upstream gets 30 seconds before the proxy answers 504.
▶ run itHaunted Appliance Support Deskgallerysource →
Every “run in studio” button in these docs ends up here: the chain runs in your browser, and each pink node is one trip through /api/jev.
route · front-deskrouteFront deskemit · book-technicianemitbook-technicianemit · forward-billingemitforward-billinggate · anyone-in-dangergateAnyone in danger?route · classify-entityrouteWhat are we dealing with?emit · book-exorcistemitbook-exorcistemit · power-cycleemitpower-cycleemit · close-windowemitclose-windowemit · evacuateemitevacuateemit · ask-daveemitask-daverepairbillingpoltergeistpossessed-firmwarejust-a-draftthenotherwiseparanormalunsure

My toaster whispers my name at 3am and the bread comes out cold.

open in studio →

Pointing the client at it#

The client builds its endpoint as baseURL + path. Point baseURL at the proxy, empty the path, and pass apiKey: null so no authorization header is sent. A browser client for this site's proxy looks like this:

jev-client.ts
import { createJev } from "jevchain";
import { jevHeaders } from "@/lib/byok";

export const jev = createJev({
  apiKey: null,          // no key in the browser, ever
  baseURL: "/api/jev",   // our route handler
  path: "",              // baseURL is already the whole endpoint
  // Add the BYOK header (if any) per request, so changing keys needs no new client.
  fetch: (url, init) => fetch(url, { ...init, headers: jevHeaders(init?.headers) }),
});

Batching, concurrency limits, timeouts and retries all still happen in the browser, before the proxy sees anything. A four-way parallel over the same state is still one request.

Building your own? The core is a dozen lines:

app/api/jev/route.ts
// app/api/jev/route.ts: the smallest proxy that works
export async function POST(req: Request) {
  const upstream = await fetch("https://api.typesafe.ai/v1/systemone", {
    method: "POST",
    headers: {
      authorization: `Bearer ${process.env.TYPESAFE_API_KEY}`,
      "content-type": "application/json",
    },
    body: await req.text(),
    signal: AbortSignal.timeout(_000),
  });

  // Pass status, body and retry-after through untouched, so the client's
  // error classes and retry logic behave exactly as if it talked to TypeSafe.
  const headers = new Headers({ "content-type": upstream.headers.get("content-type") ?? "application/json" });
  const retryAfter = upstream.headers.get("retry-after");
  if (retryAfter) headers.set("retry-after", retryAfter);
  return new Response(await upstream.text(), { status: upstream.status, headers });
}

Bring your own key#

Hit the key button in the top bar to use your own TypeSafe key instead of the shared one. Where it goes:

  • It's saved in this browser's localStorage (jevchain.byok) and nowhere else.
  • It's sent only to this site's own /api/jev, as the x-typesafe-key header, which forwards it to TypeSafe as the bearer token for that one request. The server doesn't log or store it.
  • A header that isn't plausibly a key (whitespace, or over 512 characters) is rejected with a 400. Remove the key and you're back on the shared one.

With no BYOK header and no TYPESAFE_API_KEY on the server, the proxy answers 401 missing_key, which the client surfaces as a JevAuthError. GET /api/jev is a tiny health check that says whether a shared key is configured:

health
GET /api/jev
→ { "ok": true, "serverKey": true }

Rate limiting#

shared key60 / minutePer client (first x-forwarded-for address, else x-real-ip). Keeps one tab from melting the demo key.
your key600 / minuteYou're spending your own quota; this only stops runaway loops.

The limiter is a sliding-window log: it keeps each client's request timestamps for the last 60 seconds and admits a request only while that window holds fewer than the limit. It's in memory, per server instance, so it's a guard rail, not a distributed quota. Every response past the limiter carries x-ratelimit-limit and x-ratelimit-remaining; a refusal is a 429 with retry-after:

response
HTTP/1.1 429 Too Many Requests
retry-after: 12
x-ratelimit-limit: 60
x-ratelimit-remaining: 0

{ "error": { "type": "rate_limited",
             "message": "the shared key needs a breather — 60 requests a minute per person. ..." } }

Because it's a real 429 with retry-after, the client treats it like TypeSafe's own: it waits and retries (up to maxRetryAfterMs), and records each retry in the trace. All of the proxy's own errors share the { error: { type, message } } shape, with type one of rate_limited, payload_too_large, invalid_request, missing_key or upstream_unreachable.

api key

bring your own typesafe key, or ride the shared one (rate-limited, be nice).

shared key
checking…
your key
not set

your key stays in this browser (localStorage, jevchain.byok). it only travels to this site's /api/jev proxy in an x-typesafe-key header, which forwards it to typesafe and immediately forgets it. nothing is logged or stored server-side. requests on your own key get a much roomier rate limit.

keyboard shortcuts

fewer clicks, more chains. these work anywhere outside a text field.