Skip to content
decodecodeveloper docs
Studio

Architecture

How Studio is wired end to end — edge, cloud cluster, and desktop — and how requests, runs, and sandboxes flow between the tiers.

This page describes how a running Studio deployment is wired together: the tiers, what each one does, and how a request becomes an agent run, a tool call, or a sandbox preview. It's useful background whether you self-host or use the cloud, and it's the conceptual companion to the Kubernetes and Docker Compose deploy guides.

The Kubernetes layout follows the official chart 0.16.1, reviewed at Studio commit f4cd364c. The edge diagram represents the hosted reference path; a self-hosted deployment supplies its own ingress and public routing.

The three tiers

Studio spans three trust/locality boundaries:

  • Edge — the public internet path: a CDN and an L4 load balancer.
  • Cloud cluster — the Kubernetes deployment: web, API, workers, Postgres, NATS, and cloud sandboxes.
  • Desktop — the native Studio app on the user's laptop, where its embedded local-api owns native coding harnesses and local sandboxes.

The same flow as text:

EDGE                CLOUD CLUSTER                                     DESKTOP
────                ─────────────                                     ───────

Client ─▶ CF ─┬─▶ NLB ─▶ Web(nginx) ─▶ API ──┬─▶ MCP Proxy ─▶ Downstream MCP (ext)
              │                               ├─▶ Files/Storage ─▶ Object Store (ext)
              │                               ├─▶ DB (Postgres)
              │                               ├─▶ NATS
              │                               └─▶ Worker ─┬─▶ LLM
              │                                           ├─▶ Downstream MCP (in-process bridge)
              │                                           └─▶ AgentSandbox ─┬─ Daemon API (/_sandbox/*)
              │                                                             └─ Org FS (sidecar) ─▶ /api/:org/fs ─▶ S3
              └─▶ Gateway (k8s) ─▶ Preview (public dev-server)

Desktop app ─▶ local-api ─┬─▶ Studio API (upstream data and MCP)
                          ├─▶ Claude Code / Codex / OpenCode
                          └─▶ Desktop Sandbox ─▶ Org FS (mount)

Edge

ComponentRole
CF (Cloudflare)TLS termination, static SPA caching, DDoS/bot mitigation. First hop for all traffic.
NLBL4 load balancer fronting the cluster. Routes to the web (front-door) pods; the frontDoorLabels selector decides which pods receive ingress.

Cloud cluster

The front door and workers scale separately

The chart has a main Deployment with one nginx container and two API containers in each pod. They scale together as front-door pods. Queue workers run in a separate Deployment and can scale independently of that front door.

TierDeploymentResponsibility
Webnginx in the main podServes the React SPA and reverse-proxies API and MCP routes to the API containers in that pod. Port 8080.
APIHono, STUDIO_DISPATCH_ROLE=apiHTTP routes, Better Auth, the MCP proxy, access control. Enqueues Decopilot runs onto DBOS queues and tails NATS to stream output back to the UI. Stateless. Does not run the agent loop.
WorkerHono, STUDIO_DISPATCH_ROLE=workerDequeues DBOS queues (via listenQueues) and runs the agent loop (streamText: model → tool → repeat). CPU-bound. These are the run executors; scale horizontally.

The split is by role, not by image — both run the same build. STUDIO_DISPATCH_ROLE decides whether a pod listens on the DBOS queues (worker) or only serves HTTP and enqueues (api). A single-deployment setup can use all.

The set of queues a worker pod listens on is configured by env (listenQueues). Because of that, worker pools can be split per DBOS queue — running different workflows on separate pools with their own resources and scaling.

Datastores

ComponentRole
DB (PostgreSQL, via Kysely)System of record: orgs, connections, credential vault, audit, threads + messages, and sandbox_runner_state. It also holds the DBOS queues and workflow_status journal that make runs durable and recoverable.
NATSLive messaging infrastructure with three jobs: (1) the fenced JetStream run log (decopilot.stream.<thread>) used by /stream and the durable projector; (2) cross-pod run-cancellation broadcasts; (3) JetStream KV state shared by replicas for MCP list caches and connection circuit breakers.

The Decopilot run lifecycle

  1. A message (POST /messages) or an automation fire creates a run on a thread.
  2. The API enqueues it onto a DBOS queue in Postgres:
    • THREAD_GATE_QUEUE — serialized per thread (concurrency 1 per threadId).
    • AUTOMATIONS_QUEUE — partitioned by org, so a saturated org only blocks its own partition.
  3. A Worker dequeues the gate workflow and starts a hosted child workflow. In-process Decopilot runs use HOSTED_HARNESS_QUEUE; sandbox-hosted Claude Code runs use HOSTED_HARNESS_SANDBOXED_QUEUE. The parent gate doesn't wait on the child — it live-tails the same NATS stream the child publishes to, while the durable projector writes messages and terminal status.
  4. Output chunks are published to NATS and tailed back to the UI over /stream.
  5. If the pod crashes, DBOS journal replay resumes retriable steps on another pod — recovery is the framework's job, not hand-rolled.

Both paths are hosted and use Studio's fixed AgentSandbox provider when they need a sandbox:

  • Decopilot runs its model loop in-process on the worker and reaches the managed sandbox for repository, filesystem, Git, and shell operations.
  • Claude Code runs its harness loop inside the managed sandbox; the worker proxies its stream into the same NATS and projector pipeline.

Native coding-agent chats are a separate surface. The desktop app runs Claude Code, Codex, or OpenCode locally and synchronizes the thread through its embedded local-api.

MCP: in-process vs. the proxy routes

This distinction matters for reasoning about the system:

  • The worker calls MCP tools in-process. The agent loop builds a PassthroughClient over an in-memory bridge and calls tools directly — no HTTP hop inside the cluster. It still connects outward to downstream MCP servers.
  • The MCP proxy routes are for external clients. /mcp/virtual-mcp/:id, /mcp/:connectionId, /oauth-proxy/*, and /.well-known are served by the API to external IDEs (Cursor, Claude, VS Code). The worker does not use these.

Which routes are called by whom

RoutesAPI / externalWorker
MCP proxy (/mcp/*, /oauth-proxy/*, .well-known)✅ external clients via the API❌ (in-process instead)
File & object-storage (/api/:org/files/*, presigned GET/PUT, uploads, /api/:org/fs/*)✅ serving clients✅ agent tools read/write files & mint presigned URLs
Worker-only over HTTP—none — tool calls are in-process

File and object-storage routes are the genuine "called by both" surface, and they are backed by an S3-compatible Object Store. The /api/:org/fs/* routes also back the org-filesystem mount inside sandboxes.

Sandboxes

A hosted AgentSandbox clones the repo, runs the dev server, and exposes an in-pod daemon. Its HTTP surface splits in two:

SurfaceAuthPurposeCaller
Preview (catch-all *)None — the handle (subdomain) is the secretReverse-proxies the running dev server (the live app preview); injects HMR. /_sandbox/* is actively rejected here.The end user's browser at <handle>.preview.<domain>, through Cloudflare (LB) → a Kubernetes Gateway (Istio Gateway API / HTTPRoute) → the daemon
Daemon API (/_sandbox/*)Bearer DAEMON_TOKENControl surface: fs ops (read/write/edit/bash/grep), git (status/diff/publish), exec scripts, setup (clone → install → start), tasks, SSE events, harness dispatch.The cluster (worker for agent fs/git/bash tools; API for UI setup + events)

Cloud vs. desktop sandboxes

Cloud sandboxDesktop sandbox
Whereagent-sandbox operator + a SandboxClaim pod per (user, projectRef)The native app's local-api and SandboxManager, with one local worktree per handle
Reached overk8s port-forward / in-cluster Service (control); ingress or port-forward (preview)loopback interception; <handle>.localhost:<port> (preview)
Routing ownerStudio's hosted API always uses AgentSandbox and reports agent-sandboxThe native app intercepts the same lifecycle/filesystem calls locally and reports local-api

Org filesystem (org-fs)

Each sandbox can mount the org filesystem at <appRoot>/org/<volume>, so the agent and dev server read and write org files as ordinary paths. The mount stack is rclone (NFS/FUSE) → the daemon's loopback WebDAV → /api/:org/fs/* → S3 — the same object store as the file routes, surfaced as a mounted volume. This is the same filesystem you browse in the Library.

It is wired on both surfaces, with different mount mechanics:

Cloud sandboxDesktop sandbox
Who mountsa privileged sidecar container (the unprivileged daemon can't mount)local-api directly on the user's machine
Config deliverypost-bind: Studio POST /_sandbox/orgfs-config; the daemon relays it to a shared control volume the sidecar watches (warm-pool claims reject spec.env)the native runtime resolves the org volumes and maintains the local mount
Propagationrclone with allowOther so the mount propagates to the main containersingle client — no propagation needed

Desktop

The native Studio app embeds the production web UI and an Axum local-api on one authenticated loopback origin. local-api proxies shared data and MCP traffic upstream, but terminates native-only thread, harness, sandbox, filesystem, Git, task, terminal, and preview routes on the laptop. It launches the user's Claude Code, Codex, or OpenCode CLI in the thread's local worktree and persists native thread and sandbox state in SQLite.

Provider routing follows the receiving surface rather than a request option: browser lifecycle calls reach the hosted API and AgentSandbox; the same calls from the native webview are intercepted by local-api and operate on the local-api sandbox. That keeps hosted and desktop records distinguishable without exposing a provider selector to callers.

At a glance

  • Web and API share the main Deployment; workers scale separately as DBOS run executors.
  • Runs are durable via DBOS queues + journal in Postgres; recovery is automatic.
  • MCP tool calls are in-process on the worker; the /mcp/* proxy routes are for external clients only.
  • File/object-storage routes are the shared API+worker surface.
  • NATS supports streaming and cross-pod coordination.
  • Sandbox routing is surface-owned: hosted Studio uses AgentSandbox; the native app intercepts locally and records local-api.