Browser Access & Deployment¶
Cortex ships a browser workbench — the same SPA the desktop app wraps — reachable from any browser without installing anything. This page covers both the deployment runbook (building the SPA and enabling the Web UI endpoint on your server) and the browser access path (reaching it through Cloudflare Access edge login).
There are two independent ways to reach the workbench, and they authenticate differently:
| Path | Who | Authentication | Holds clientToken? |
|---|---|---|---|
| Browser | Anyone with a browser | Cloudflare Access edge login (email / IdP), verified as a JWT by the server | No — the browser never sees the token |
| Desktop (Tauri) | The installed desktop app | Bearer x-cortex-token (the clientToken) stored in the OS keychain |
Yes |
This page is the browser + deployment reference. For installing the native desktop app, see Desktop App.
The in-core Web UI transport¶
The Web UI transport ships in-core with @cortex-agent/server and is loaded on demand
behind the CORTEX_UI_HTTP flag. It pulls @trpc/server and jose, but these are
runtime-lazy: the server imports the transport only through a dynamic import that is reached
solely when the flag is set. When the flag is unset, neither the transport code nor @trpc/server
/ jose ever enter the runtime module graph, so a Slack- or TUI-only deployment pays zero UI
weight — while installing and upgrading stay a single npm install -g @cortex-agent/server.
When enabled, the transport:
- serves the built SPA (
web/dist) same-origin with the tRPC API — one port, one origin, so the browser loadsindex.html+ assets and calls/trpcwith no cross-origin plumbing; - exposes the tRPC API at
/trpc(HTTP batch for queries/mutations, SSE for subscriptions), connected in-process directly to the server's domain services (no proxy, no sidecar); - gates every
/trpcrequest behind a dual-path auth check (see Authentication).
It runs in the same process as the server, binds to 127.0.0.1, and is meant to be exposed
to the internet only through a tunnel.
Deployment runbook (source → running server)¶
These steps take you from a fresh checkout to a Cortex server serving the browser workbench.
1. Prerequisites¶
- Node.js ≥ 20 and pnpm (this repo is a pnpm workspace).
- A running Cortex server (see Quickstart).
2. Build the SPA¶
The web SPA is built to web/dist. From the repo root:
pnpm install # install workspace dependencies
pnpm -r run build # build every workspace package, including web → web/dist
To build only the SPA and its dependencies:
web/dist is a plain static bundle (index.html + hashed assets). When the @cortex-agent/server
package is published, web/dist is staged into the package (via a prepack step) and shipped in
its files, so an installed server can serve the SPA with no extra build. From a source checkout
you build it yourself with the step above.
3. Where web/dist is served from¶
The server resolves the SPA directory in this order:
- an explicit directory you pass to it;
- the
CORTEX_UI_SPA_DIRenvironment variable; - the package-root
web/dist(present in an installed/published package); - the monorepo's
web/dist(when running from a source checkout).
If you deploy an installed package, option 3 works out of the box. If you deploy from a repo
checkout, option 4 works with no configuration — build web/dist in place and it is served
automatically. If you deploy the built SPA to a different location, point CORTEX_UI_SPA_DIR at
it. If the directory is absent (SPA not built), non-/trpc paths return a 404 placeholder while
/trpc still works.
4. Enable the Web UI endpoint¶
Add to ~/.cortex/config/.env on the server:
CORTEX_UI_HTTP=1 # opt-in: start the tRPC HTTP + SSE endpoint (required)
CORTEX_UI_PORT=3004 # optional; defaults to 3004
CORTEX_UI_HTTP accepts 1, true, on, or yes. With it unset, the endpoint — and the
Web UI transport, along with @trpc/server / jose — never loads.
5. Restart the daemon to apply¶
The daemon reads the SPA and env at startup, so a new build or env change takes effect on restart:
Restarting is disruptive
Restarting the daemon interrupts any in-flight agents, threads, and scheduled work. Treat it as a deliberate, controlled action — schedule it when the system is idle, and if your Cortex instance runs under an approval policy, route the restart through that approval step rather than restarting ad hoc.
At this point the server is reachable at http://127.0.0.1:3004 on the server host only.
Exposing it to a browser is the next section.
Authentication¶
The /trpc auth gate accepts a request if either credential is valid, and returns 401
before tRPC runs otherwise:
x-cortex-tokenheader equal to the server'sclientToken— the desktop / machine path. Checked first, with a constant-time comparison. Unchanged from before.- A valid
Cf-Access-Jwt-Assertionheader — the browser path. Cloudflare's edge injects this JWT after it authenticates the user; the server verifies it. The browser never holds theclientToken.
The server verifies the Access JWT against your Cloudflare Access team-domain JWKS, checking the signature (RS256 / ES256 only), the audience (AUD) tag, the issuer, and expiry. If Access is not configured on the server, the JWT path is disabled and the gate securely degrades to token-only — an unconfigured Access path never admits a request.
Browser access via Cloudflare Access¶
The browser path puts a Cloudflare Access edge login in front of a dedicated UI hostname, so users log in with email / IdP at the edge and the server only ever sees an already-verified request.
browser
│ https (Cloudflare Access: email / IdP login at the edge)
▼
Cloudflare Tunnel (cortex-ui.example.com → server localhost:3004)
│ edge injects Cf-Access-Jwt-Assertion on every request
▼
agent-server Web UI transport (verifies the JWT; the browser never holds clientToken)
├─ serves web/dist (same-origin SPA)
└─ serves /trpc (same-origin real data, in-process)
1. Create a dedicated UI hostname and tunnel route¶
Point a Cloudflare Tunnel route from a new public hostname (for example
cortex-ui.example.com) to the server's loopback endpoint (http://127.0.0.1:3004, or your
CORTEX_UI_PORT).
Use a separate hostname from the cortex-client endpoint
The hostname your remote cortex-client instances connect to must not be placed behind
Cloudflare Access — Access would block the WebSocket clients. Always give the browser UI its
own hostname and apply Access only to that one.
2. Add a Cloudflare Access application (account-side ops)¶
In the Cloudflare Zero Trust dashboard, create a self-hosted Access application for the UI hostname with a policy that allows your login email (or IdP group). This is an account-level operation performed in the Cloudflare dashboard, not in Cortex config. Note the application's AUD tag — you need it below.
3. Configure the server to verify Access JWTs¶
Add to ~/.cortex/config/.env on the server:
CORTEX_ACCESS_TEAM_DOMAIN=your-team # bare team name, or your-team.cloudflareaccess.com
CORTEX_ACCESS_AUD=<your-access-app-AUD> # the Access application's AUD tag
# CORTEX_ACCESS_CERTS_URL=... # optional: override the derived JWKS URL
From CORTEX_ACCESS_TEAM_DOMAIN the server derives the issuer
(https://your-team.cloudflareaccess.com) and the JWKS URL
(https://your-team.cloudflareaccess.com/cdn-cgi/access/certs). If either
CORTEX_ACCESS_TEAM_DOMAIN or CORTEX_ACCESS_AUD is unset, the browser path stays disabled
(token-only). Restart the daemon after changing these values.
4. Open the workbench¶
Navigate to https://cortex-ui.example.com. Cloudflare Access challenges you for email / IdP
login; after you authenticate, the edge forwards every request with a verified
Cf-Access-Jwt-Assertion, the server serves the same-origin SPA, and the workbench loads real
tRPC data — no token, no local install.
Browser path vs desktop bearer-token path¶
Both paths reach the same /trpc API and the same workbench, but they differ in where and
how they authenticate:
| Browser | Desktop (Tauri) | |
|---|---|---|
| Hostname | Dedicated UI hostname behind Cloudflare Access | A hostname not behind Access |
| Login | Cloudflare Access edge login (email / IdP) | Enter serverUrl + clientToken once |
| Credential on requests | Cf-Access-Jwt-Assertion (issued by the edge) |
x-cortex-token header |
| Where auth is checked | JWT verified by the server | Token verified by the server |
clientToken exposure |
Never touches the browser | Stored in the OS keychain |
| SPA origin | Same-origin (SPA + /trpc on one host) |
Direct connection to /trpc (CORS-enabled) |
Because the desktop app sends x-cortex-token, it must connect through a hostname that is
not behind Cloudflare Access (Access would block the bearer request at the edge). The
browser path is the opposite: Access does the login, and the browser gets in without ever
holding the token. Choose the desktop app when you want a native window and are comfortable
storing the token locally; choose the browser path when you want zero install and IdP-managed
access.
Troubleshooting¶
Browser gets a Cloudflare login loop or 403 at the edge
The Access application policy does not allow your identity. Check the policy on the UI hostname's Access application in the Cloudflare Zero Trust dashboard.
Browser logs in at the edge but the workbench shows 401 / no data
The edge authenticated you but the server rejected the JWT. Confirm on the server that
CORTEX_ACCESS_TEAM_DOMAIN matches your team and CORTEX_ACCESS_AUD matches the Access
application's AUD tag exactly, and that the daemon was restarted after setting them. With those
unset, the browser path is disabled and every browser request is 401.
The page loads but /trpc calls 404
web/dist was not found, so only the API is served. Build the SPA (pnpm --filter web run
build) or point CORTEX_UI_SPA_DIR at your built bundle, then restart.
Nothing is reachable at the UI hostname
- Confirm
CORTEX_UI_HTTP=1is set and the daemon was restarted after adding it. - On the server, confirm the endpoint is up:
curl http://127.0.0.1:3004/trpcshould return a tRPC error (not connection-refused). - Confirm the Cloudflare Tunnel is running and its route points at the correct port.