Embedded Web UI¶
Cortex ships the operator web UI inside the cortex binary. The same process that
serves the HTTP API (/api/*) and the readiness probe (/health) also serves the
compiled Next.js export at /, so there is no separate web container and no
second port. The Go handler in internal/web reads the assets embedded with
go:embed, gates the surface with a dedicated web access key, and injects the
runtime API endpoint through /config.js.
The web access key is a credential in its own namespace (
internal/webkey). It is never used as, defaulted to, or derived fromhttp.tokenor the replication token, and it is never accepted by the/api/*endpoints.
Quick start¶
# 1. Boot the local SQLite server. The embedded UI is mounted on the HTTP listener.
cortex serve
On the very first boot Cortex mints a web key and prints it exactly once:
Web access key generated on first boot.
<key>
Store it in a safe place; it will not be shown again.
mode=local
store=~/.cortex/cortex.db
sync=disabled
web=mounted
key_file=~/.cortex/web.key
listen=localhost:7438
# 2. Open the UI and paste the key once.
http://localhost:7438/
The key entry screen stores the key in the browser (localStorage key
cortex_web_key), probes /health and then the guarded surface, and never
prompts again. An incorrect paste is rejected with an actionable error that points
at cortex web key regenerate.
After the first boot the plaintext is never printed again: later boots reuse the
persisted key silently. The default listener is http://localhost:7438
(http.host/http.port); the web surface inherits that host and port.
Key lifecycle¶
Commands¶
cortex web key show # where the key lives, whether it exists, its prefix
cortex web key regenerate # mint a replacement and print the new plaintext once
Both commands accept an explicit store path:
cortex web key show --key-file /path/to/web.key
cortex web key regenerate -k /path/to/web.key
show reports the key file path, whether a key exists, and the public prefix; it
never prints the plaintext. When no key exists it reports the absence and the
first-boot behavior without creating one.
regenerate mints a fresh key, persists it atomically, and prints the new
plaintext exactly once:
Web access key regenerated.
Key file: ~/.cortex/web.key
New web access key (shown once):
<key>
Store it in a safe place; it will not be shown again.
The previous key is no longer valid.
At-rest format¶
The key store persists only a non-secret record — never the plaintext — in
web.key (default: ~/.cortex/web.key):
prefix=ctx_XXXXXXXX
digest=<base64url-hmac-sha256>
- The plaintext secret is 32 random bytes from
crypto/rand, encoded asctx_+ base64 (raw URL alphabet). Theprefixline is the first 12 characters of that secret;digestis an HMAC-SHA256 over the secret keyed by a value derived from the prefix. - Verification uses prefix lookup followed by a constant-time comparison
(
hmac.Equal), so a tampered digest rejects deterministically. - The file is created with mode
0600inside a0700directory. On POSIX systems a key file that is group/other readable fails closed on load (permissions are not silently repaired); Windows skips the POSIX mode check. - A failed first-boot write never leaves a partial file behind.
Rotation semantics¶
- First boot uses an exclusive create (
O_EXCL), so concurrent boots mint exactly one key: the winner prints the plaintext and the loser loads the winner's key without printing anything. regeneratewrites a temporary file in the same directory, fsyncs it, and atomically renames it overweb.key. If any step fails, the previous key stays valid and unchanged and the command exits non-zero.
Modes¶
The mode triad is a closed set: local, hybrid, server
(internal/platform/mode.go). The embedded web surface is mounted in all three;
what differs is the backing store, the API authorization, and how the surface is
wired.
--mode local (default) |
--mode hybrid |
--mode server |
|
|---|---|---|---|
| Backing store | SQLite (~/.cortex/cortex.db) |
SQLite (identical composition) | PostgreSQL (self-hosted single-tenant) |
| Web surface | Mounted at / on the local HTTP listener |
Mounted at / on the local HTTP listener |
Mounted at / on the server HTTP listener |
| API auth | Static http.token bearer |
Static http.token bearer |
Static http.token bearer + synthetic principal + RLS/authorized stores |
| Data endpoints | /api/* (SQLite), including the five parity routes below |
/api/* (SQLite) + replication loop |
Same five parity routes via internal/api + Streamable HTTP MCP /mcp |
| Web key file | ~/.cortex/web.key (or --key-file) |
Same as local | Same default path |
| Status banner | mode=local store=<path> sync=<enabled\|disabled> web=mounted |
mode=local … sync=enabled (hybrid sets CORTEX_MODE=hybrid) |
cortex: web <baseURL>/, cortex: web key file <path> |
- local — the single-binary SQLite composition with the local stdio MCP
server.
cortex serveboots the HTTP API and mounts the embedded UI on the same listener. Hybrid is not a distinct store: it is this composition with the remote sync/replication loop layered on. If--mode hybridis selected but sync is disabled or unconfigured, startup degrades to local behavior with an explicit warning naming the missing sync configuration. - server — the self-hosted single-tenant PostgreSQL composition.
cmd/cortex(the only permitted local→server bridge) builds the sameinternal/webhandler and mounts it ahead of the authenticated/api/and/mcproutes. The request plane authenticates one configured static bearer (http.token) against a synthetic constant service principal assembled fromserver.tenant_id,server.workspace_id, andserver.principal_subject; there is no per-request tenant or workspace derivation. The credential is minted only after the server composition opens, so a failed bootstrap never creates a key file or claims a surface. Tenant and workspace remain configuration constants enforced through the server's authorized stores and forced RLS; the web key is an independent UI credential.
The five web-critical endpoints are served by the local serve mux behind the
same withAuth token gate, backed by a bundle port over the SQLite composition
(REQ-SH-012):
GET /api/me— synthetic local principal (owner role, single-user local mode).GET /api/stats— counters recomputed from the local bundle.GET /api/projectsandGET /api/agent/projects— derived from sessions, observations, and code.GET /api/graph/project-graph— BFS over graph edges honoringdepthandmax_nodes.
/api/audit, /api/system/metrics, and /api/rag/stats remain deferred roadmap
items and are not registered on the local mux (no local audit store exists).
Because hybrid and local mount the surface identically, a browser session that works against a local server also works against a hybrid one; the UI simply sees the replication state through the API.
Runtime endpoint injection¶
The Go server serves the API origin to the browser as a script, so the UI is not pinned to a build-time URL:
GET /config.js— unauthenticated,application/javascript,no-cache; defineswindow.__CORTEX_WEB_CONFIG__.- Default (shipped compositions) resolves the page origin, keeping the surface
same-origin:
window.__CORTEX_WEB_CONFIG__ = { "serverUrl": window.location.origin };. - The client (
web/src/lib/server-endpoint.ts,resolveServerEndpoint) readswindow.__CORTEX_WEB_CONFIG__.serverUrlwhen present and otherwise falls back to the documented defaulthttp://localhost:7438. /healthis served unauthenticated so the shell can render before a key is entered./api/*is never answered by the web handler — the API mux owns that namespace and keeps its own bearer semantics.
For contributors¶
web/ is a Next.js app exported statically (output: "export"), not run as a
Node server. The build pipeline is ordered so the Go binary always compiles, even
in a checkout where the web toolchain never ran:
# Run this BEFORE make build / release builds.
make web-build
make web-build:
- runs
npm cionly whenweb/node_modulesis absent, - runs
npm run buildinweb/(producingweb/out/), - mirrors
web/out/intointernal/web/dist/, removing prior export output but preserving the committed placeholderinternal/web/dist/index.html.
The placeholder keeps go:embed and go build ./... green before any web build.
go:embed uses the all: prefix so the _next/ asset tree is embedded intact.
.gitignore ignores the synced export (/internal/web/dist/*) while re-including
the placeholder (!/internal/web/dist/index.html), so bulk output is never
tracked. For the full gate, run make web-build then make build; release builds
follow the same ordering.
Troubleshooting¶
- Lost the key. Run
cortex web key regenerateon the host, then paste the new key in the UI. The previous key stops verifying immediately after the atomic replace;cortex web key showconfirms where the file lives and its prefix. - Server refuses to start.
cortex serverefuses to expose the HTTP API (and therefore the web surface) on a non-loopback host unlesshttp.tokenis configured. Bindlocalhostfor local use, or sethttp.tokenintentionally. - UI reports an invalid key. The presented key did not verify (HTTP 401). The
inline error points at
cortex web key regenerate; the stale value is not stored. - Permission error on load. On POSIX the key file must not be group/other
readable. Move it to a private directory and
chmod 600; the store fails closed rather than repairing the mode. - Boot fails with "web access key unavailable". The key store could not be
created or read (for example an unwritable data directory).
serveand--mode serverabort non-zero instead of silently serving an API-only surface.