Secure your frontend
In shortHow a browser app talks to a production 10xGraph server without a secret: server-side proxy or short-lived user tokens, WebSocket auth, remote tools, error codes.
- 4 min read
- 8 sections
- Updated
- v1.0.0rc4
- Markdown
A browser is an untrusted place. Anything you ship to it, including environment variables, can be read by the person using it. The server side of 10xGraph is built around that: the server checks every request against the caller’s identity, and the client’s job is to carry that identity safely. This page covers how to do that.
What the server already enforces
You do not need to rebuild these in your frontend. With auth configured and MODE=production, 10xgraph-api does the following (details in the security model):
- Rejects requests without valid credentials with
401. - Lets a user reach only their own threads. A foreign
invokeorstreamis refused with403before it reaches the graph, and an id the server did not create is refused too (runs.thread_idsdefaults toserver). - Applies the
rate_limitwindow and a per-user cap on runs in flight with429. - Accepts a client message only with the
userrole, and atoolmessage only as the answer to a pending client tool call.
The frontend must still get the credential to the server without leaking it.
Rule 1: no server secret in the browser
A shared API key or service token gives anyone who copies it the same access as your whole app. It must never reach browser code. You have two safe patterns.
Option A: a server-side proxy (recommended)
Your browser calls a route on your own backend. That route checks the user’s session, adds the token from a server-only variable, and forwards the call to 10xGraph. The browser never learns the token or the 10xGraph URL. The Next.js and React guide shows the full pattern with a route handler that streams the response back.
Option B: short-lived tokens issued per user
If the browser must call the 10xGraph server directly, for example to open a WebSocket, issue each user their own short-lived token from your login endpoint. With "auth": "jwt" the token carries a user_id claim, and the server derives ownership from it. Fetch the token at runtime after the user signs in, hold it in memory, and refresh it before it expires. See Create and configure the client for rotation. To cut a token off before it expires, configure token_revocation (see auth).
Never put a token in a NEXT_PUBLIC_ variable. Those values are compiled into the bundle at build time, so every visitor gets the same one and anyone can read it.
Also set ORIGINS on the server to your exact frontend origins. With MODE=production, the server refuses to start when ORIGINS is *.
Rule 2: WebSocket authentication
A browser cannot set an Authorization header on a WebSocket. wsStream() therefore opens the socket with the subprotocol list ['10xgraph-bearer', '<token>']. The server reads the token from the entry after the sentinel and echoes the sentinel back, which the browser requires to finish the handshake. The token is never placed in the URL, so it stays out of access logs.
- This needs
10xgraph-api0.7.0 or later. The client sends only the10xgraph-bearersentinel. - On Node the client also sets an
Authorizationheader. - The server re-checks revocation before every WebSocket run when
token_revocationis configured.
If the handshake fails, see client troubleshooting.
Rule 3: remote tools run in the browser, so their results are input
Client-side tools keep the schema on the server and the implementation in the browser. That is useful, and it also means the result comes from a place the user controls. The server treats it that way: a tool message is accepted only as the answer to a remote tool call the graph has pending. A client cannot inject a tool result on its own, and it cannot send assistant or system messages.
Still treat the result as untrusted data in your graph. Validate it before it drives a payment or a write, the same way you would validate a form field.
Handling error responses
Each status maps to a class from 10xgraph-client. Full table: Handle errors.
| Status | Class | What it means for your UI |
|---|---|---|
401 |
AuthenticationError |
Token missing, invalid, expired or revoked. Refresh the token once, then send the user to sign in. |
403 |
PermissionError |
The scope is missing, or the thread_id belongs to another user or does not exist. Do not retry. For a new conversation, omit thread_id and use the id the server returns. |
409 |
ConflictError |
The thread is busy with another run, or a resume’s interrupt_id does not match. Wait and retry, or send on_busy: 'cancel' to stop the running turn first. The default on_busy: 'wait' holds the request up to 30 seconds before it answers 409. |
429 |
TenxGraphError with statusCode 429 |
The rate limit window is used up, or the user has too many runs in flight. |
For 429, the client has no dedicated class and does not keep the Retry-After header. A rate-limit response ends its message with Retry after <n>s., which you can parse. The run-cap response has no Retry-After header. Back off with a delay that grows each attempt and stop after a few tries. A working example is in Rate limits (429).
import { TenxGraphError } from '10xgraph-client';
try {
await client.invoke(messages, { config: { thread_id } });
} catch (err) {
if (err instanceof TenxGraphError && err.statusCode === 429) {
// wait, then retry with backoff; do not loop immediately
} else {
throw err;
}
}Checklist
- No token in any
NEXT_PUBLIC_variable or committed file. - Browser calls go through your proxy, or use a short-lived per-user token fetched at runtime.
ORIGINSlists your real frontend origins.- WebSocket users run
10xgraph-api0.7.0 or later. - Your UI handles
401,403,409and429as above. - You ran the pre-production smoke tests.
Next steps
- Production readiness: what you get when you serve with
10xgraph-api. - Production checklist: server-side settings to confirm before launch.
- What 10xGraph does not protect: including running with
auth: null.
Frequently asked questions
- Can I put the API token in a NEXT_PUBLIC variable?
- No. NEXT_PUBLIC variables are written into the JavaScript bundle at build time, so every visitor can read them and they cannot hold a per-user value. Keep a shared token on a server, or issue each user a short-lived token at runtime after they log in.
- How does a browser authenticate a WebSocket?
- Browsers cannot set headers on a WebSocket. The client sends the bearer token as the second entry of the Sec-WebSocket-Protocol list, after the 10xgraph-bearer sentinel. The token never goes in the URL. This needs 10xgraph-api 0.7.0 or later.
- Does the client retry a 429 for me?
- No. It throws a plain TenxGraphError with statusCode 429 and does not keep the response headers, so the Retry-After value is not available as a field. Back off in your app.