API and realtime transport#

The Go backend serves the embedded SPA, JSON HTTP endpoints, tus uploads, and the application's WebSocket routes from one server.

Request path#

flowchart LR Client["Browser client"] --> Caddy["Caddy HTTPS"] Caddy --> Middleware["Auth and onboarding middleware"] Middleware --> Handler["HTTP or WebSocket handler"] Handler --> Access["Role and project access checks"] Access --> Service["Application service"] Service --> Store["File store"] Service --> Integration["LXD, Git, tmux, host filesystem"]

All /api/* and /ws* requests require a signed session for a registered user. They are also blocked until local-admin setup and at least one module declared as an access-gate provider is ready, except the module-driven agent-auth catalog, streams, and compatibility auth routes needed to finish onboarding.

Authentication routes#

MethodRoutePurpose
GET/auth/meCurrent auth and onboarding status
POST/auth/local/claimCreate the local administrator or complete legacy setup
POST/auth/local/loginLocal administrator password login
GET/auth/google/loginStart Google OAuth, optionally preserving a safe return URL
GET/auth/google/callbackValidate OAuth state, authorize invited email, and issue session
GET/auth/logoutClear platform cookies and return to the app
GET/auth/verifyCaddy forward-auth check; preview hosts also check project membership
GET, PUT/api/admin/auth/googleRead or replace Google OAuth configuration; admin only

Users and settings#

MethodRoutePurpose
GET, POST/api/admin/usersList or add registered users; admin only
DELETE/api/admin/users/{email}Remove a user; admin only
PUT/api/admin/users/{email}/rolePromote or demote a user; admin only
GET, PATCH/api/me/settingsRead or update current user's appearance and chat defaults
GET/api/server/infoHost, CPU, memory, storage, network, and process snapshot

Agent authentication, capabilities, and skills#

MethodRoutePurpose
GET/api/agent-authOrdered module descriptors plus normalized current auth snapshots; available before the provider gate opens
GET/api/{provider}/auth-statusLegacy provider-specific status payload for an available managed binding; external bindings return null
POST/api/{provider}/login/startStart or resume a managed authorization-code flow; currently Claude; admin only
POST/api/{provider}/login/codeSubmit a managed authorization code; currently Claude; admin only
POST/api/{provider}/login/cancelCancel a managed authorization-code flow; currently Claude; admin only
POST/api/{provider}/login/deviceStart a managed device flow; currently Codex and Kimi; admin only
POST/api/{provider}/login/api-keyValidate and save a managed provider API key; currently MiniMax; admin only; response never includes the key
DELETE/api/{provider}/login/api-keyRemove a managed provider API key; currently MiniMax; admin only
GET/api/agent-capabilities[?projectId=<id>&refresh=1]Discover normalized provider/model controls on the host or in an accessible project; refresh=1 bypasses the current backend cache entry
GET/api/skills?provider=...&projectId=...List accessible provider and project skills

The module descriptor chooses one auth mode: managed authorization code, managed device flow, managed API key, external, or none. Route registration follows the built auth binding, so a provider exposes only operations valid for its declared flow. MiniMax uses a write-only managed API-key binding whose status exposes only configured/unconfigured. Antigravity uses an external binding with no observable status, status stream, or managed host-login route; users authenticate it with agy inside each project.

GET /api/agent-auth is the frontend's authoritative auth registry. Each row contains provider, label, optional default, executionScopes, an authentication object (mode, optional instructions, and satisfiesAccessGate, and optional API-key creation metadata), and a normalized status object. Status contains authenticated, an optional warning, and one login shape shared by managed code/device flows (active, URL, optional code/timestamps/completion/error). Managed API-key status contains only the configured boolean. No-auth modules report authenticated immediately. External modules publish their instructions but no managed status stream or mutation controls.

The auth registry is separate from model capability caching. Each GET /api/agent-auth reads current binding snapshots, and managed changes arrive through WebSockets; these values do not wait for the 24-hour/2-hour model-catalog TTL.

The access gate uses descriptor policy rather than a fixed provider list. A no-auth module marked satisfiesAccessGate opens it immediately; managed code/device/API-key modules require an authenticated binding. External authentication cannot satisfy the gate because Remote cannot observe it reliably. The current built-ins mark Claude, Codex, and Kimi as gate providers; MiniMax is managed but not a gate provider, and Antigravity is external.

The capability response has a providers array in registry order. Each provider includes its source (live or fallback), optional warning and structured unavailability reason, models, per-model reasoning efforts and service tiers, modes, model/control defaults, the module default flag, execution scopes, authentication metadata, and declared session/skill/browser/scheduling/execution-policy features. Omitting projectId selects the host/loose-chat scope. Supplying it requires admin status or project membership and selects that project's current container. Only the literal refresh=1 forces discovery; other values use the normal cache path. Refresh bypasses a completed entry but joins a same-scope discovery already in flight. The route does not start a stopped project.

The cache is backend-process memory, shared across web clients by execution scope. Fully live, warning-free results use a 24-hour TTL; any fallback or warning shortens the complete scope to 2 hours. A backend restart clears it. See Capability discovery for provider probes and all refresh triggers.

Each provider probe receives the same AGENT_CAPABILITY_TIMEOUT deadline. The default is 30 seconds for the provider's complete discovery operation; setting the Go-duration environment value to 0 disables the deadline. Providers are still probed concurrently, so the setting is not multiplied into a sequential whole-catalog timeout.

Project routes#

MethodRoutePurpose
GET, POST/api/projectsList visible projects or create a project
POST/api/projects/reorderUpdate project ordering
GET, PATCH, DELETE/api/projects/{id}Read, rename, or admin-delete a project
POST/api/projects/{id}/startStart or relaunch a project
POST/api/projects/{id}/stopStop a project
POST/api/projects/{id}/restartForce restart or relaunch a project
GET/api/projects/{id}/containerDetailed container inspection
PUT/api/projects/{id}/limitsSet CPU, memory, and disk overrides; admin only
POST/api/projects/{id}/repair-networkReconfigure container networking and reinspect
GET/api/projects/{id}/appsList externally reachable container listeners
GET/api/projects/{id}/agent-browserGet Agent Browser core/view status and record activity
POST/api/projects/{id}/agent-browser/startEnsure Agent Browser is starting or ready
DELETE/api/projects/{id}/agent-browserStop the complete Agent Browser
DELETE/api/projects/{id}/agent-browser?scope=viewStop only the noVNC view
GET/api/projects/{id}/secretsList project secrets
PUT, DELETE/api/projects/{id}/secrets/{key}Set or delete one secret
GET, POST/api/projects/{id}/accessList members or add a registered email
DELETE/api/projects/{id}/access/{email}Remove a member
GET/internal/tls-ask?domain=...Caddy allow-check for on-demand project certificates

Every {id} project route first requires admin status or project membership. Resource-limit changes and project deletion add an admin-only check.

Chat routes#

MethodRoutePurpose
GET, POST/api/chatsList visible chats or create a chat
GET, PATCH, DELETE/api/chats/{id}Read, update, or delete chat metadata/history
GET/api/chats/{id}/events?limit=&before=Page persisted events backward by sequence
GET/api/chats/{id}/transcript?limit=&before=Page complete transcript turns backward; adjacent text deltas are compacted
POST/api/chats/{id}/rewindRemove a selected prompt and later events
POST/api/chats/{id}/forkCopy metadata/history and defer provider-session fork
POST/api/chats/{id}/readMark current history read
POST/api/chats/{id}/unreadForce unread state
GET/api/chats/{id}/ide-open?path=...Validate path and redirect to the correct IDE URL
GET/api/chats/{id}/media-open?path=...Serve supported workspace media inline
GET/api/chats/{id}/files?path=...List a workspace directory
GET/api/chats/{id}/files/search?q=...Search workspace filenames
GET/api/chats/{id}/files/download?path=...Download one file
GET/api/chats/{id}/files/download-folder?path=...Stream a folder ZIP
GET/api/chats/{id}/history/reposDiscover workspace Git repositories
GET/api/chats/{id}/history/commits?repo=&limit=List commits
GET/api/chats/{id}/history/diff?repo=&sha=Read one commit patch
POST/api/chats/{id}/history/checkoutOptional checkpoint and detached checkout
GET, POST/api/chats/{id}/schedulesList the caller's tasks for a project chat, or create one through the user API

All chat routes resolve the caller and enforce the chat's project membership. Loose chats have no project membership check.

Scheduled-task routes#

MethodRoutePurpose
PATCH, DELETE/api/schedules/{id}Edit/pause/resume or delete a visible owned task; admins can manage all
POST/api/schedules/{id}/runRequest an immediate occurrence without moving its regular deadline
GET, POST/agent-api/schedulesList or create tasks inside the capability's chat/project fence
PATCH, DELETE/agent-api/schedules/{id}Pause or delete a capability-scoped task; an agent cannot enable it
POST/agent-api/schedules/{id}/runRequest a capability-scoped immediate occurrence
POST/agent-api/schedules/current/completeComplete only the task/run named by a complete-self capability

Browser routes use the signed user session. Agent routes require a short-lived bearer capability issued for one owner, chat, and project; they do not accept a platform session cookie. Agent-created tasks are forced to createdByAgent and start disabled until a user arms them.

Schedule request bodies cap at 64 KiB and reject unknown fields. Stored prompts cap at 32 KiB. The service re-checks the owner, chat, project, registration, and access on every fire.

Upload and auxiliary routes#

MethodRoutePurpose
POST, HEAD, PATCH, GET, DELETE/api/uploads[/<upload-id>]tus resumable upload lifecycle
GET/__remote_inspectorSame-origin preview inspection wrapper
GET, POST/api/sessionsList or create host tmux sessions
DELETE/api/sessions/{name}Delete tmux session
POST/api/sessions/{name}/sendSend text into tmux session
POST/api/sessions/{name}/uploadMultipart upload into tmux working directory

The upload access check happens when the random upload URL is created. Later chunk requests rely on possession of that URL.

WebSocket routes#

RouteDirectionMessages
/ws/workspaceServer to clientSnapshot, chat upsert/delete, project upsert/delete
/ws/chat/{id}?since=<seq>BothClient prompt or cancel; server chat events and sync
/ws/terminal?chat={id}BothPTY binary data; JSON input and resize control
/ws/agent-auth/{provider}Server to clientNormalized auth snapshots for an available managed auth binding
/ws/{provider}/auth-statusServer to clientLegacy provider-specific auth status payloads
/ws?session={name}BothAuxiliary tmux PTY binary data and control messages

Chat and project-terminal membership is checked before the WebSocket upgrade. Removing a member prevents future checked connections, but the backend does not currently close or reauthorize that member's already-open sockets.

Realtime channels#

flowchart TD Browser["Browser"] --> WorkspaceWS["Workspace WebSocket"] Browser --> ChatWS["Active chat WebSocket"] Browser --> TerminalWS["Optional terminal WebSocket"] Browser --> AuthWS["/ws/agent-auth/provider while onboarding or in Settings"] WorkspaceWS --> WorkspaceHub["Workspace hub"] ChatWS --> RunHub["Per-chat run hub"] TerminalWS --> PTY["lxc exec PTY"] AuthWS --> AuthService["Provider auth subscription"] WorkspaceHub --> Repositories["Repository notifications"] RunHub --> EventStore["Persisted JSONL events"] RunHub --> Prompt["Prompt start and cancel"]

Chat reconnect and replay#

sequenceDiagram participant UI participant HTTP as Events API participant WS as Chat WebSocket participant Store as Event store UI->>HTTP: Load latest event page HTTP->>Store: Read bounded page Store-->>UI: events, lastSeq, nextBefore, hasMore UI->>WS: Connect with since=lastSeq WS->>Store: Read events after sequence Store-->>WS: Missed events WS-->>UI: Replay, then sync state, then live events WS--xUI: Connection drops UI->>WS: Exponential reconnect with latest applied sequence

The workspace and chat streams send ping frames every 25 seconds. The frontend chat socket reconnects from 400 ms up to 5 seconds and requests only unseen sequences.

Chat event shapes#

EventMain fieldsPersisted
usertextYes
assistant_texttext, optional messageIdYes
thinkingtextYes
tool_use_startid, name, inputYes
tool_use_endid, output, isErrorYes
systemsubtype, dataYes
sessionprovider and provider session IDYes
completeusage payloadYes
errormessageUsually yes; lock/contention errors may be transient
syncrunningNo

Common status behavior#

  • 400: invalid IDs, paths, values, or JSON.
  • 401: missing or invalid session.
  • 403: valid user without role or project access.
  • 404: missing chat, project, user, file, or repository target.
  • 409: running-chat conflict, dirty Git state, protected last-admin/member guardrail, duplicate project display name, or duplicate user.
  • 412: onboarding gate is incomplete.
  • 413: upload or request body is too large.

Code map#

  • Route composition: backend/internal/transport/transport.go
  • HTTP server: backend/internal/transport/http/server.go
  • Frontend route constants: frontend/src/config/routes.ts
  • Chat socket: backend/internal/transport/ws/chat_socket.go
  • Workspace socket: backend/internal/transport/ws/workspace_socket.go
remote.futrx documentation