Skip to content

HTTP endpoints

The server is a single Hono app: one process serves the SPA, the tRPC API, three WebSocket surfaces, and a handful of plain REST endpoints for uploads, auth, and integrations. Every surface (web, desktop, mobile, extension) is a thin client over this same API. See Architecture overview for how requests flow into the command model.

Unclaimed instances (zero ops.users rows) resolve every request to a legacy full-access principal, so nothing below is gated. Once an instance is claimed (first successful login), anonymous requests get 401 on every tRPC procedure except health.ping, plus explicit guards on /files, /blobs, /mail/proxy-image, and /oauth/*. WebSocket upgrades on /ws, /pty, and /meetings/audio are rejected before the handshake completes. See Identity for the full model.

Route Method Access Purpose
/ GET public Serves the SPA (apps/web/dist).
/health GET public Liveness probe: {ok: true}.
/trpc/* ALL authenticated The tRPC 11 API. Every read/write model in the product (kb, mail, tasks, agents, and so on) rides through here.
/ws WS upgrade authenticated The topic-bus WebSocket: outbox-derived live updates (pages, mail, run trees, notifications). See Outbox and dispatcher.
/pty WS upgrade admin-only Terminal WebSocket: attaches to a PTY session on the server host. Rejected pre-handshake for non-admins.

POST /files  multipart/form-data

Uploads a file: the body lands in the content-addressed blob store (blobs/ab/cd/<sha256>) before a kb.files row commits. Returns {id, sha256, name, mime, size}. Authenticated.

GET /files/:id/content  path param: id

Streams a file’s content by its kb.files row id. Readable by the file’s owner or through any readable page that references it (kb.file_refs); an unreadable row 404s exactly like a missing one, no existence probe.

GET /blobs/:sha256  path param: sha256

Streams a blob directly by content hash. Readable only if some readable (or unowned) kb.files row carries that hash. Immutable: served with a one-year cache-control: private, immutable header.

GET /provider-sync  query: none

Status of connected provider mirrors (Google Drive, Dropbox, SharePoint) visible to the caller. Authenticated.

POST /provider-sync/:provider  path param: provider (google-drive | dropbox | sharepoint), query: account?

Manually pulls one connector. Admin-only.

POST /files/:id/provider  multipart/form-data

Writes a local file’s content back upstream to its connected provider (CAS write-back, conflict-checked via If-Match/Dropbox update rev).

GET /mail/proxy-image  query: url

Server-side image proxy for click-to-load remote images in mail bodies. HTTPS-only, private hosts refused (SSRF guard), no credentials forwarded, 5s timeout, 10 MB cap, image/* content types only. Authenticated.

Route Method Access Purpose
/auth/google/start GET public Redirects to Google OAuth (openid email profile); accepts ?invite=<code>. 501 when Google login isn’t configured.
/auth/google/callback GET public Google OAuth callback: sets the subspace_session HttpOnly cookie.
/auth/dev-login POST public, PROVIDERS=fake only {email} logs in (or claims the instance) without Google. The e2e/sandbox login seam; 403 outside PROVIDERS=fake.
/auth/logout POST authenticated Destroys the session and clears the cookie.
/auth/me GET public {user, mode, devLogin, googleLogin}. Open by design: the login page needs this pre-auth.
/pair/claim POST public {code, name, kind?} claims a one-shot, 5-minute device pairing code minted from Settings → Devices and returns a bearer device token exactly once. See Device pairing.

GET /oauth/gmail/start  query: email?

Redirects into the Gmail OAuth consent flow for the signed-in user. 501 when no Gmail provider is configured (for example under PROVIDERS=fake). Authenticated.

GET /oauth/gmail/callback  query: code, state

Gmail OAuth callback: exchanges the code, stamps the connecting user as the account owner, redirects to /. Authenticated.

POST /gitops/webhook  body: forge webhook payload

Public endpoint a forge push hook calls to trigger an immediate sync instead of waiting for the poll interval. Verified constant-time as a GitHub/Gitea X-Hub-Signature-256 HMAC or a GitLab X-Gitlab-Token header against the configured webhook secret; no secret configured means 403 for everyone. The sync runs detached (202 {queued: true}) and webhook bursts coalesce on an advisory lease. See GitOps.

WS /meetings/audio  WebSocket upgrade

Streams live meeting audio into the speech-to-text pipeline. Rejected pre-handshake when unauthenticated. See Meetings.

These routes are never exposed beyond the host process boundary; they exist for internal process-to-process calls (the worker role talking to the web role, or the workflow executor talking back to the server that spawned it).

Route Purpose
/internal/broker/handshake Protocol version + readiness check between the web and worker roles.
/internal/broker/available Reports whether a named capability is grantable, without acquiring it.
/internal/broker/rpc Coarse capability RPCs (mail send, IM send) proxied from the worker to the web plane, which holds the credentials.
/internal/broker/stdio-env Returns a stdio MCP server’s declared, resolved environment at spawn time only (the explicit scoped exception to “never ambient”).
/internal/broker/anthropic/v1/*, /internal/broker/openai/v1/* LLM proxy: injects the provider API key host-side so it never reaches the worker’s environment.
/internal/broker/mcp/:server, /internal/broker/mcp/:server/* HTTP MCP proxy with headers injected host-side.
/internal/broker/egress General HTTP egress boundary for platform (mlops) API calls, credentials injected host-side.
/.well-known/workflow/v1/{flow,step} The Workflow DevKit executor’s callback surface: graphile-worker POSTs durable step results back to this process here. See Agent engine.