deco 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 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.
Edge public internet Cloud Cluster kubernetes Desktop native app · laptop Cloud Sandbox · pod Object Store s3 · external LLM anthropic · external Downstream MCP tool servers · external Client browser / mcp CF edge / cdn NLB l4 lb Web nginx · spa ×N · scales independently API hono · role=api ×N · scales independently Worker role=worker · dbos ×N · scales independently MCP Proxy api routes Files / Storage api · /files /fs NATS messaging DB postgres Gateway k8s gateway api Daemon API protected · /_sandbox/* Org FS mount · sidecar Preview public local-api axum · embedded Native Harness pty · local cli Desktop Sandbox local-api Org FS mount · daemon
EdgeWebAPIWorkerMCPFilesPostgresNATSSandboxPreviewLLMDesktop
Request path, agent runs and sandboxes across the edge, cloud cluster and desktop tiers.

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

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

Cloud cluster

Web, API, and Worker scale independently

These three deployments are separate and scale on their own. The web tier was split out from the API (the “front-door split”) so the static front door can deploy and scale on a different cadence than the application server.

Tier Deployment Responsibility
Web nginx Serves the React SPA and reverse-proxies /api , /mcp , /oauth-proxy , /.well-known to the API Service. Port 8080.
API Hono, STUDIO_DISPATCH_ROLE=api HTTP 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.
Worker Hono, STUDIO_DISPATCH_ROLE=worker Dequeues 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

Component Role
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.
NATS Live 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 event bus is dormant. The CloudEvents pub/sub feature ( EVENT_PUBLISH / EVENT_SUBSCRIBE , the durable event queue, ON_EVENTS subscribers) is only consumed by the workflow plugin, which is not in use. NATS itself is not dormant — it serves the live jobs listed above. Don’t conflate the two.

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

Routes API / external Worker
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:

Surface Auth Purpose Caller
Preview (catch-all * ) None — the handle (subdomain) is the secret Reverse-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_TOKEN Control 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 sandbox Desktop sandbox
Where agent-sandbox operator + a SandboxClaim pod per (user, projectRef) The native app’s local-api and SandboxManager , with one local worktree per handle
Reached over k8s port-forward / in-cluster Service (control); ingress or port-forward (preview) loopback interception; <handle>.localhost:<port> (preview)
Routing owner Studio’s hosted API always uses AgentSandbox and reports agent-sandbox The 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 sandbox Desktop sandbox
Who mounts a privileged sidecar container (the unprivileged daemon can’t mount) local-api directly on the user’s machine
Config delivery post-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
Propagation rclone with allowOther so the mount propagates to the main container single 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 / API / Worker are independent deployments; workers are 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 is live; the CloudEvents event-bus feature is dormant.
  • Sandbox routing is surface-owned: hosted Studio uses AgentSandbox; the native app intercepts locally and records local-api .

Found an error or want to improve this page?

Edit this page