# Calling Seclai from a Browser

You don't. Your page calls your backend, and your backend calls Seclai.

```
Browser  ──►  Your server route  ──►  api.seclai.com
   ▲                  │
   └────── SSE ───────┘

           the API key lives here, and only here
```

No Seclai credential — [API key](https://seclai.com/docs/api-keys) or [OAuth Bearer token](https://seclai.com/docs/api-introduction#authentication) — can be narrowed for a browser. There are no per-resource, per-operation, or read-only scopes: a credential grants **everything its identity is allowed to do**. An account-scoped API key is the whole account's API surface. A user-scoped key or an OAuth token is authorized against that user's role in the account, so it carries every permission that user holds — which for an owner or admin is, again, the whole account.

Requests are still authorized: a viewer member cannot mutate through a leaked token any more than they can through the UI. That is the floor, and it is not low enough. A viewer can read, and a credential that reaches the browser has reached the user, their extensions, and anyone who opens the network tab — so the most restricted identity you can issue still hands over every knowledge base it can see, which is the loss that matters (see [below](#why-there-is-no-cors)).

So the Seclai API sends no CORS headers, and browser JavaScript cannot call it. That is a decision, not an oversight. The rest of this page explains it and shows what to build instead.

---

## Why there is no CORS

The obvious request is "let me allowlist my origin and call the API directly." Three things make that a worse deal than it looks.

**An origin restriction is not a security control.** CORS is enforced by the browser, on behalf of the page. It is not enforced by the server against the caller. A script, a `curl` command, or a mobile app sends whatever `Origin` header it likes, and the server cannot tell a real page from a forged header. Allowlisting `https://app.example.com` would stop your own page from making mistakes; it would not stop anyone who copied the credential out of it.

**Retrieval is an exfiltration primitive.** "Read-only" sounds like the safe thing to hand a browser. For a knowledge base it is the dangerous one. A credential that can only run retrieval against a single knowledge base can still be run in a loop, and enough queries reconstruct the corpus. When the proprietary content is the asset, read access _is_ the loss. A shorter expiry does not change that; it only bounds how long each theft has to run.

**A proxy is not technically hard here.** Browser-held credentials are justified when a proxy is impossible, not when it is inconvenient. The industry exception is ephemeral keys for realtime WebRTC APIs, where the media stream genuinely cannot route through an HTTP proxy. Seclai is request/response HTTP plus SSE, and a proxy relays both fine. Anyone shipping a production widget already has a backend — which is also where they want rate limiting, caching, per-user auth, and abuse controls, none of which a browser credential can provide.

---

## Allowlist, not passthrough

This is the most important warning on the page.

The tempting shortcut is a generic proxy — one route that forwards any path, method, and body through to `api.seclai.com` with the key attached:

```javascript
// DO NOT DO THIS.
export async function POST({ request, params }) {
  return fetch(`https://api.seclai.com/${params.path}`, {
    method: request.method,
    headers: { "X-API-Key": process.env.SECLAI_API_KEY },
    body: request.body,
  });
}
```

That is **strictly worse than enabling CORS would have been**. It turns an unscoped API key into an unauthenticated public endpoint: anyone who finds the route has the account's full API surface, with no key to steal and no origin to spoof, and your server pays for the credits. The key never leaks, which is exactly why nothing looks wrong.

A proxy should expose **named operations**, not paths. "Moderate this photo." "Answer this question against knowledge base X." "Stream a run of agent Y with this input." Each one pins its own agent or knowledge base, accepts one fixed shape of input, and validates it. If the caller gets to name the agent, the knowledge base, or the endpoint, you have built a passthrough with extra steps.

---

## What a proxy owes you

A checklist for anyone rolling their own:

1. **Server-side key.** Read the API key from the environment on the server. Never return it in a response, never inline it into rendered HTML, and make sure the route is not prerendered into the client bundle.
2. **A small allowlist of named operations.** Not a path passthrough. See above.
3. **Rate limiting and a spend ceiling.** Per session or per IP, plus a hard cap on what the whole route may spend in a window. A public endpoint that runs agents is a public endpoint that spends credits.
4. **Upload validation before anything reaches Seclai.** Enforce a byte cap and a MIME allowlist on your side, and reject empty files. Do it before the upload leaves your server, so a rejected file costs nothing.
5. **An SSE relay for streaming runs.** Consume the Seclai stream on the server and re-emit your own events to the browser, carrying only the fields the UI needs. The relay is also where you decide what a client is allowed to see.
6. **Sanitized errors.** Seclai error payloads can carry account detail. Log the original server-side and return your own short message with a status code; never echo the upstream body to the browser.

> **Serverless counts as a backend.** "My app has no backend" usually means "my app has no server I maintain." A Vercel or Netlify function, a Cloudflare Worker, an Astro or Next.js server route, or a Lambda behind API Gateway is all it takes. What you need is a place to keep a secret and run a few checks, not a fleet.

---

## Worked examples

[listing-moderator](https://github.com/seclai/demos/tree/main/listing-moderator), in the open-source [github.com/seclai/demos](https://github.com/seclai/demos) repository, is a working example of this shape: a browser page that uploads a photo, an Astro server route that runs a Seclai agent over it, and results streamed back as SSE.

The parts worth copying:

- **`src/pages/api/moderate.ts`** — one named operation, not a passthrough. It declares `prerender = false` so the route runs on the server, caps the upload at 20 MB, allows only JPEG and PNG, and rejects empty files before the bytes ever reach Seclai. The browser cannot choose the agent, the model, or the endpoint.
- **`src/lib/seclai.ts`** — the single place that reads `SECLAI_API_KEY` from the environment and constructs the client, so no page or component can reach the API by accident.
- **The SSE relay** — the route consumes the Seclai run on the server and emits its own `result` and `error` events, rather than piping the upstream stream through to the browser. See [Agent Streaming](https://seclai.com/docs/agent-streaming) for the upstream events being relayed.

Treat it as a starting point rather than a finished template: a demo is not carrying production traffic, so the rate limiting, spend ceiling, and error sanitizing from the checklist above are yours to add.

---

## Common questions

**Can I get a short-lived, origin-scoped token for the browser?** No. Scoping and expiry each address the wrong half of the problem: a retrieval-scoped token still exfiltrates the knowledge base while it lives, and an origin restriction does not survive leaving the browser. This was proposed and declined for those reasons.

**What about a public demo where I don't care about the data?** Build the same proxy. The credential is bound to the account, not to the one knowledge base you are comfortable exposing, so a leak from a throwaway demo is a leak of everything else in that account. If a demo must be genuinely disposable, give it its own account and its own key.

**Can I use OAuth from the browser instead?** No. The API sends no CORS headers regardless of credential type, so the call cannot be made. A Cognito access token is also not the narrower credential it looks like: it carries every permission the signed-in user holds in the account, with no per-resource scope to attach to it. OAuth is for MCP clients and for server-side integrations acting on a user's behalf.

**Does the MCP server change the answer?** No. `/mcp` authenticates with the same credentials and is likewise not reachable from browser JavaScript. See [MCP Server](https://seclai.com/docs/mcp).

---

## Next Steps

- [API Introduction](https://seclai.com/docs/api-introduction) — base URL, authentication, and rate limits
- [API Keys](https://seclai.com/docs/api-keys) — creating, rotating, and monitoring keys
- [API Examples](https://seclai.com/docs/api-examples) — the request and response shapes to wrap in your named operations
- [Agent Streaming](https://seclai.com/docs/agent-streaming) — the SSE events your relay consumes
