A Durable-Object-backed orchestrator that runs Claude Code sessions on a VPS and lets any browser watch, control, and approve from anywhere — even after you close the tab.
Before diving in, build a mental model. Duraclaw is a hotel that runs AI sessions for guests.
| Hotel | Duraclaw | Why it matters |
|---|---|---|
| The guest | You, in your browser | Can leave and come back. Nothing's stored on your device. |
| Front desk | Cloudflare Worker | Auth, routing, assigns you a "room" (SessionDO). |
| Your room | SessionDO (Durable Object) | Private, persistent. Exists when you're not in it. |
| Room service | cc-gateway on VPS | The hands that actually run Claude Code. |
| Intercom | WebSocket | Two-way real-time conversation. |
| Staff hallway | Cloudflare Tunnel | Private, secure passage. Guests never see it. |
| Room logbook | SQLite inside the DO | Every message written down forever. |
Each layer runs on different infrastructure, has different responsibilities, and talks to its neighbors through specific channels.
Cloudflare Workers can't do what Claude Code needs:
Workers have no persistent disk. Claude needs to read and write files.
git, npm, python, pytest — Workers can't exec processes.
Workers cap at ~30s. Claude sessions run minutes to hours.
Real repositories, with history, on real disk with real worktrees.
What happens when you type "fix the login bug" and hit send:
If Claude wants to edit a file, the chain pauses at step 5 and inserts an approval prompt (see Permissions).
The single most important class in Duraclaw. One instance per AI session, persists for its entire lifetime.
Ask for DO session-abc-123 and you always get the same global instance.
One copy worldwide. Requests processed in order. No race conditions.
Each DO has its own SQL database. Survives restarts and data-center moves.
DOs hold open sockets — the foundation for real-time messaging.
Schedule "wake me up at X" callbacks. Used for reconnection retries.
Idle DOs evict; on next request they spin back up and read state from SQLite.
SessionDO doesn't extend raw DurableObject. It extends Agent from the agents package, which gives browser-side the useAgent() React hook — typed, auto-reconnecting WebSocket with no manual management.
Need to add a column to session SQLite? Migrations are versioned and idempotent — run every wake-up, only new ones execute.
A Bun server on your VPS. The SessionDO is the brain; cc-gateway is the hands.
The gateway binds to localhost — no one on the internet can connect to it directly. Your VPS firewall can block all inbound traffic. The only way in is via Cloudflare Tunnel (outbound from VPS).
Two message families flow over the DO ↔ gateway WebSocket, defined in shared-types.
| Command | Meaning |
|---|---|
| execute | Start a new Claude session in this worktree |
| resume | Resume a previous session using its SDK session ID |
| abort | Cancel the current session |
| answer | Reply to a question Claude asked |
| Event | Meaning |
|---|---|
| session.init | Claude ready, here's the model and available tools |
| assistant | Claude said something (streaming text) |
| tool_result | A tool call finished, here's the result |
| user_question | Claude is asking the user, need an answer |
| result | Session complete. Duration and cost included. |
| error | Something broke |
Orchestrator and cc-gateway never import from each other. They share only the shared-types package — a no-runtime-code, types-only package that both sides depend on. Change a type; if either side is out of sync, the TypeScript compiler refuses to build.
The gateway scans /data/projects/baseplane* for git worktrees (configurable via WORKTREE_PATTERNS). Each worktree is an independent working copy of the repo — six sessions can edit six branches at once without stepping on each other.
cc-gateway runs under systemd as duraclaw-cc-gateway.service. Auto-starts on boot, auto-restarts on crash.
Claude runs with full power on your VPS. Before any tool fires, a human approves or denies it from the browser.
bypassPermissions mode — the SDK itself doesn't block anything. The gateway adds its own approval layer on top, giving you capability without autonomy.Gateway strips CLAUDECODE* env vars before spawn — Claude can't launch Claude.
CC_GATEWAY_SECRET checked on WS upgrade using constant-time compare.
Tool call lives in DO state. User can walk away; approve from phone later.
Approvals, denials, and results all written to DO SQLite.
Deploy any AI agent that takes actions (not just generates text) — financial transactions, infrastructure changes, code edits, database modifications. The pattern is capability-without-autonomy: give the model everything, hold the trigger in a human hand.
Two chained WebSockets and one outbound-only tunnel. The whole pipeline, end to end.
| HTTP | WebSocket |
|---|---|
| Ask, answer, close | Stays open indefinitely |
| Client initiates every exchange | Either side sends at any time |
| Like sending letters | Like a phone call |
| Good for: page loads, form posts | Good for: chat, streaming, real-time |
Claude streams tokens. The browser must receive them as they're generated — no polling, no "any new tokens?" loop. That's WebSocket.
The gateway binds to 127.0.0.1. The Worker runs on Cloudflare. How do they meet?
VPS firewall can block all inbound. Tunnel is outbound only.
Cloudflare handles certs. wss:// encrypted end to end.
Traffic flows through Cloudflare's network, which mitigates automatically.
VPS handles compute. Cloudflare handles networking and security.
In the Worker config, the tunnel URL is stored as the CC_GATEWAY_URL secret.
Everything outside the core runtime: how the code is organized, how users log in, and how the team ships features.
pnpm manages workspace linking ("@duraclaw/shared-types": "workspace:*" resolves to the local folder). turborepo builds packages in dependency order and caches aggressively.
Better Auth is a TypeScript auth library (login, cookies, sessions). D1 is Cloudflare's serverless SQLite. Together they handle user accounts, login sessions, and auth tokens.
Routes live at /api/auth/*. The auth instance is created per-request because D1 is only available within a request context. Gateway auth is separate — a bearer token (CC_GATEWAY_SECRET) checked with timing-safe compare during WS upgrade.
| Tech | What it is | Where used |
|---|---|---|
| TypeScript 5.8 | JS with types | Everywhere |
| React 19 | UI library | Browser |
| TanStack Start | React meta-framework, file-based routing | Browser |
| Vite 7 | Build tool + dev server | Browser |
| Cloudflare Workers | Serverless edge functions | Orchestrator |
| Durable Objects | Stateful Workers + SQLite | Session management |
| Agents SDK | Real-time DO + React hook | SessionDO + browser |
| Better Auth | TS auth library | User login |
| Drizzle ORM | TS ORM for SQL | D1 auth DB |
| D1 | Cloudflare's SQLite | User accounts |
| Hono | Lightweight Worker framework | API routes |
| Bun | JS runtime, faster than Node | VPS gateway |
| Claude Agent SDK | Anthropic's programmatic Claude | VPS gateway |
| Vercel AI SDK | Framework-agnostic AI toolkit | Chat transport |
| Zustand | Lightweight state management | Browser UI |
| Radix UI | Unstyled accessible primitives | UI components |
| Tailwind 4 | Utility CSS | Styling |
| Biome | Linter + formatter | Code quality |
| Turborepo | Monorepo build orchestration | Build pipeline |
| pnpm | Workspace-aware package manager | Dependencies |
| Wrangler | Cloudflare CLI | Dev + deploy |
Not part of the runtime — a CLI for the team building Duraclaw. It choreographs the dev process into 8 modes, each with completion gates and verification evidence.
Design features, write specs, break work into phases.
Write the actual code.
Investigate APIs, explore options, read docs.
Execute a specific, bounded task.
Fix a specific bug.
Run verification against the real system.
Open-ended work that doesn't fit other modes.
Learn the codebase — like this guide.
The rule: prove things work by testing the real system, not mocks. Every phase produces verification evidence in .kata/verification-evidence/.
| Command | What it verifies |
|---|---|
| pnpm verify:dev:up | Starts local gateway + orchestrator |
| pnpm verify:preflight | curl, jq, agent-browser installed |
| pnpm verify:auth | Signs in a test user, saves cookies |
| pnpm verify:gateway | Real API calls to gateway endpoints |
| pnpm verify:session | Creates a real session, polls completion |
| pnpm verify:browser | Opens real browser, logs in via /login |
| pnpm verify:browser:session | Opens a session page, checks live output |
| pnpm verify:smoke | Runs all of the above in sequence |
Deployment is two separate things — Orchestrator to Cloudflare, Gateway to VPS. Then six patterns you can steal for your own projects.
Per-session, per-user, per-room DOs. Self-contained state machines with private SQLite. No race conditions, built-in persistence.
Typed, auto-reconnecting WebSocket between browser and DO. No manual connection management.
Custom AI SDK transport over WebSocket instead of HTTP streaming. Works around Worker streaming limitations.
Bind to 127.0.0.1 only. Use tunnel for external access. Zero open ports. Defense in depth for privileged services.
Intercept PreToolUse, relay to UI, gate on user decision. AI proposes, human disposes.
Real HTTP calls, real browser, real API responses. Not mocks. Especially valuable when AI writes code.
| Term | Meaning |
|---|---|
| DO | Durable Object — stateful, single-instance Worker |
| SessionDO | The DO class that manages one Claude Code session |
| cc-gateway | Bun server on VPS that runs Claude Code |
| Worker | Serverless function on Cloudflare edge |
| D1 | Cloudflare serverless SQL (used for auth) |
| CF Tunnel | Encrypted outbound tunnel from VPS to Cloudflare |
| Agents SDK | Cloudflare's DO + React hook package |
| Claude Agent SDK | Anthropic's programmatic Claude SDK |
| useAgent() | React hook connecting to a DO via WebSocket |
| Worktree | Git feature: multiple checkouts of same repo |
| Kata | Development workflow CLI (not runtime) |
| Eviction | Cloudflare shutting down an idle DO |
| Alarms API | Lets a DO schedule a wake-up callback |
| GatewayCommand | Worker → VPS message ("do this") |
| GatewayEvent | VPS → Worker message ("this happened") |
| Idempotent | Running twice = running once |
| State machine | Defined states + defined transitions |