Overview
Architecture
Durable Objects
Gateway
Permissions
Transport
Stack
Deploy
GitHub App
CLAUDE CODE · SESSION ORCHESTRATOR

Duraclaw

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.

One sentence: Duraclaw is a remote control panel for Claude Code — it decouples where the AI work happens (a server) from where you watch and interact with it (any browser).
Cloudflare Workers
Durable Objects + SQLite
Agents SDK
Claude Agent SDK
WebSocket
CF Tunnel

The Hotel Analogy

Before diving in, build a mental model. Duraclaw is a hotel that runs AI sessions for guests.

HotelDuraclawWhy it matters
The guestYou, in your browserCan leave and come back. Nothing's stored on your device.
Front deskCloudflare WorkerAuth, routing, assigns you a "room" (SessionDO).
Your roomSessionDO (Durable Object)Private, persistent. Exists when you're not in it.
Room servicecc-gateway on VPSThe hands that actually run Claude Code.
IntercomWebSocketTwo-way real-time conversation.
Staff hallwayCloudflare TunnelPrivate, secure passage. Guests never see it.
Room logbookSQLite inside the DOEvery message written down forever.

Explore the Guide

Architecture
Three layers: browser, Cloudflare, VPS. Plus a click-to-response trace.
Durable Objects
The core primitive. Identity + memory + per-object SQLite.
Gateway
cc-gateway runs Claude. Localhost-only. Worktree-aware.
Permissions
Human-in-the-loop approval for every tool call.
Transport
Two WebSockets chained through a DO. Plus CF Tunnel.
Stack
Monorepo, Better Auth + D1, tech choices, Kata CLI.
Deploy
Ship Worker + VPS + Tunnel. Plus 6 patterns to steal.

Quick Links

GitHub
codevibesmatter/duraclaw
Durable Objects
Cloudflare docs
Agents SDK
useAgent() hook
Claude Agent SDK
Anthropic docs
02 · ARCHITECTURE

Three Layers

Each layer runs on different infrastructure, has different responsibilities, and talks to its neighbors through specific channels.

TOPOLOGY YOU (Phone / Laptop / Desktop) │ │ HTTPS + WebSocket (public internet)┌─────────────────────────────────┐ │ CLOUDFLARE Worker + Durable Objects │ └─────────────────────────────────┘ │ │ WebSocket over CF Tunnel (private, encrypted)VPS cc-gateway + Claude Agent SDK
1 · Browser (your device)React + TanStack Start — dumb viewport, stores nothing
2 · Cloudflare Worker + DOServes the site, auth (D1), manages SessionDO instances
3 · VPS (cc-gateway)Bun server running Claude, real filesystem, real shell
Key decision: the browser stores nothing. All state lives on the server. Close the tab, switch devices, come back — everything is still there.

Why a VPS?

Cloudflare Workers can't do what Claude Code needs:

Real filesystem

Workers have no persistent disk. Claude needs to read and write files.

Shell commands

git, npm, python, pytest — Workers can't exec processes.

Long runtime

Workers cap at ~30s. Claude sessions run minutes to hours.

Project codebases

Real repositories, with history, on real disk with real worktrees.

Data Flow: Click to Response

What happens when you type "fix the login bug" and hit send:

1 · BrowseruseAgent() sends { type: "session.message", content } over WS
2 · SessionDOWrites to SQLite, forwards to VPS on WS #2
3 · cc-gatewayParses GatewayCommand, hands content to Claude SDK
4 · Claude SDKThinks, streams tokens, may request tool use
5 · cc-gatewayEmits { type: "session.message.delta", content } per token
6 · SessionDOStores + relays to browser on WS #1
7 · BrowserReact renders deltas — the "typing" effect

If Claude wants to edit a file, the chain pauses at step 5 and inserts an approval prompt (see Permissions).

03 · DURABLE OBJECTS

SessionDO

The single most important class in Duraclaw. One instance per AI session, persists for its entire lifetime.

A regular Worker is a cashier — serves whoever's next, remembers nothing. A Durable Object is a personal banker — you're assigned to one, they know your history, you always talk to the same one.

What DOs Give You

Identity

Ask for DO session-abc-123 and you always get the same global instance.

Single-threaded

One copy worldwide. Requests processed in order. No race conditions.

Private SQLite

Each DO has its own SQL database. Survives restarts and data-center moves.

WebSocket native

DOs hold open sockets — the foundation for real-time messaging.

Alarms API

Schedule "wake me up at X" callbacks. Used for reconnection retries.

Eviction-safe

Idle DOs evict; on next request they spin back up and read state from SQLite.

What SessionDO Owns

Session stateinitializing → active → paused → terminated
Message history (SQLite)Every message, tool call, tool result — forever
WebSocket relayHolds WS #1 (browser) and WS #2 (VPS) simultaneously
Reconnect alarmsIf VPS drops, schedules retry via Alarms API

The Agents SDK Base Class

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.

// browser side — the entire connection in one line const { messages, send } = useAgent({ name: "session-abc-123" })

DO Migrations

Need to add a column to session SQLite? Migrations are versioned and idempotent — run every wake-up, only new ones execute.

const migrations = [ { version: 1, sql: `CREATE TABLE messages (...)` }, { version: 2, sql: `ALTER TABLE messages ADD COLUMN tool_results TEXT` }, ]; export function runMigrations(sql: SqlStorage) { const current = sql.exec("PRAGMA user_version").one()?.user_version ?? 0; for (const m of migrations) { if (m.version > current) { sql.exec(m.sql); sql.exec(`PRAGMA user_version = ${m.version}`); } } }
Idempotent = running twice gives the same result as running once. No duplicate "Phone" columns, no matter how many times the DO wakes up.
04 · GATEWAY

cc-gateway

A Bun server on your VPS. The SessionDO is the brain; cc-gateway is the hands.

Bun runtime
127.0.0.1:9877
systemd service
Zero public ports

Why 127.0.0.1 Only

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).

Why this matters: the gateway has the power to run arbitrary code via Claude. Localhost-only binding + outbound-only tunnel is defense in depth — you can't reach it even if you try.

The Protocol

Two message families flow over the DO ↔ gateway WebSocket, defined in shared-types.

Commands (Worker → VPS)

CommandMeaning
executeStart a new Claude session in this worktree
resumeResume a previous session using its SDK session ID
abortCancel the current session
answerReply to a question Claude asked

Events (VPS → Worker)

EventMeaning
session.initClaude ready, here's the model and available tools
assistantClaude said something (streaming text)
tool_resultA tool call finished, here's the result
user_questionClaude is asking the user, need an answer
resultSession complete. Duration and cost included.
errorSomething broke

shared-types: The Contract

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.

// shared-types/src/index.ts (simplified) export type GatewayCommand = | { type: "session.create"; config: SessionConfig } | { type: "session.message"; content: string } | { type: "session.tool.approve"; toolCallId: string } | { type: "session.tool.deny"; toolCallId: string; reason: string } | { type: "session.terminate" }; export type GatewayEvent = | { type: "session.created"; sessionId: string } | { type: "session.message.delta"; content: string } | { type: "session.tool.request"; toolCall: ToolCall } | { type: "session.complete"; summary: string };

Worktrees

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.

Running as a Service

cc-gateway runs under systemd as duraclaw-cc-gateway.service. Auto-starts on boot, auto-restarts on crash.

# install ./packages/cc-gateway/systemd/install.sh # manage systemctl start duraclaw-cc-gateway systemctl status duraclaw-cc-gateway journalctl -u duraclaw-cc-gateway -f
05 · PERMISSIONS

Human-in-the-Loop

Claude runs with full power on your VPS. Before any tool fires, a human approves or denies it from the browser.

AI proposes, human disposes. The gateway runs Claude in bypassPermissions mode — the SDK itself doesn't block anything. The gateway adds its own approval layer on top, giving you capability without autonomy.

The Flow

TOOL APPROVAL ROUND-TRIP Browser ──▶ SessionDO ──▶ cc-gateway ──▶ Claude Agent SDKPreToolUse eventcc-gateway ◀── Claude Agent SDK │ ▼ Browser ◀── SessionDO ◀── cc-gateway (tool approval prompt: "Claude wants to edit login.ts") │ ▼ user clicks Approve or Deny Browser ──▶ SessionDO ──▶ cc-gateway ──▶ Claude Agent SDK (tool runs or aborts)

Step-by-step

1. Claude emits PreToolUseIntercepted by cc-gateway before execution
2. Gateway pauses the sessionTool call is queued, nothing runs yet
3. user_question event firesSent to SessionDO over WS #2
4. DO relays to browserStored in SQLite, forwarded on WS #1 — shows as prompt
5. User approves or denies{ type: "session.tool.approve" | "deny", toolCallId }
6. Gateway resolves the gateTool executes — or returns a denial to Claude

Safe by Default

No nested sessions

Gateway strips CLAUDECODE* env vars before spawn — Claude can't launch Claude.

Timing-safe bearer auth

CC_GATEWAY_SECRET checked on WS upgrade using constant-time compare.

Paused ≠ lost

Tool call lives in DO state. User can walk away; approve from phone later.

Full audit trail

Approvals, denials, and results all written to DO SQLite.

When to Steal This Pattern

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.

06 · TRANSPORT

WebSockets + CF Tunnel

Two chained WebSockets and one outbound-only tunnel. The whole pipeline, end to end.

HTTP vs WebSocket

HTTPWebSocket
Ask, answer, closeStays open indefinitely
Client initiates every exchangeEither side sends at any time
Like sending lettersLike a phone call
Good for: page loads, form postsGood 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 Chain

TWO WEBSOCKETS, ONE DO IN THE MIDDLE Browser ═══WS 1═══▶ SessionDO ═══WS 2═══▶ cc-gateway (VPS) WS 1: public internet, via Cloudflare WS 2: over Cloudflare Tunnel — private, encrypted
Why put the DO in the middle? If WS 1 drops (browser closes), the DO still owns WS 2 — Claude keeps working. When the browser reconnects, the DO replays missed messages from SQLite. The middleman is what makes "close the tab and come back" possible.

Cloudflare Tunnel

The gateway binds to 127.0.0.1. The Worker runs on Cloudflare. How do they meet?

1. Install cloudflared on VPSConnects outward to Cloudflare — no inbound ports
2. Cloudflare assigns a hostnamee.g. gateway.yourdomain.com
3. Worker connects to wss://gateway.yourdomain.comCloudflare routes through tunnel to 127.0.0.1:9877
Analogy: your VPS is a house with no doors. The tunnel is dug from inside the house to a Cloudflare relay station. People can't walk up to your house, but they can visit the relay, which sends messages through the tunnel. The house initiated it, so the house is in control.

What You Get

Zero open ports

VPS firewall can block all inbound. Tunnel is outbound only.

Free TLS

Cloudflare handles certs. wss:// encrypted end to end.

DDoS protection

Traffic flows through Cloudflare's network, which mitigates automatically.

Clean separation

VPS handles compute. Cloudflare handles networking and security.

In the Worker config, the tunnel URL is stored as the CC_GATEWAY_URL secret.

07 · STACK

Monorepo, Auth, Tools

Everything outside the core runtime: how the code is organized, how users log in, and how the team ships features.

Monorepo Layout

duraclaw/ apps/ orchestrator/ ← Cloudflare Worker + React UI src/server.ts ← Main entry: DO classes + TanStack handler wrangler.toml ← Worker configuration packages/ cc-gateway/ ← The VPS executor src/server.ts ← Bun WebSocket server systemd/ ← Linux service files shared-types/ ← Types shared by both sides src/index.ts ← GatewayCommand, GatewayEvent kata/ ← Development workflow CLI ai-elements/ ← Shared AI UI components planning/ ← Specs, roadmap, progress scripts/verify/ ← Verification scripts .kata/verification-evidence/ ← Proof things actually work

pnpm manages workspace linking ("@duraclaw/shared-types": "workspace:*" resolves to the local folder). turborepo builds packages in dependency order and caches aggressively.

Auth: Better Auth + D1

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.

D1 vs DO SQLite — what's the difference? D1 is a shared database for the whole app. One database, all users. Used for auth. DO SQLite is per-Durable-Object. Each SessionDO has its own private SQLite — used for that session's message history. D1 is the hotel's guest registry; DO SQLite is the diary inside each room.
1. Visit /loginEmail + password form
2. Worker checks D1Better Auth handler validates credentials
3. Cookie issuedSession cookie sent to browser
4. Every request validatedCookie checked on each Worker invocation

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 Stack

TechWhat it isWhere used
TypeScript 5.8JS with typesEverywhere
React 19UI libraryBrowser
TanStack StartReact meta-framework, file-based routingBrowser
Vite 7Build tool + dev serverBrowser
Cloudflare WorkersServerless edge functionsOrchestrator
Durable ObjectsStateful Workers + SQLiteSession management
Agents SDKReal-time DO + React hookSessionDO + browser
Better AuthTS auth libraryUser login
Drizzle ORMTS ORM for SQLD1 auth DB
D1Cloudflare's SQLiteUser accounts
HonoLightweight Worker frameworkAPI routes
BunJS runtime, faster than NodeVPS gateway
Claude Agent SDKAnthropic's programmatic ClaudeVPS gateway
Vercel AI SDKFramework-agnostic AI toolkitChat transport
ZustandLightweight state managementBrowser UI
Radix UIUnstyled accessible primitivesUI components
Tailwind 4Utility CSSStyling
BiomeLinter + formatterCode quality
TurborepoMonorepo build orchestrationBuild pipeline
pnpmWorkspace-aware package managerDependencies
WranglerCloudflare CLIDev + deploy

Kata: The Dev Workflow CLI

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.

planning

Design features, write specs, break work into phases.

implementation

Write the actual code.

research

Investigate APIs, explore options, read docs.

task

Execute a specific, bounded task.

debug

Fix a specific bug.

verify

Run verification against the real system.

freeform

Open-ended work that doesn't fit other modes.

onboard

Learn the codebase — like this guide.

Verification-Driven Development

The rule: prove things work by testing the real system, not mocks. Every phase produces verification evidence in .kata/verification-evidence/.

CommandWhat it verifies
pnpm verify:dev:upStarts local gateway + orchestrator
pnpm verify:preflightcurl, jq, agent-browser installed
pnpm verify:authSigns in a test user, saves cookies
pnpm verify:gatewayReal API calls to gateway endpoints
pnpm verify:sessionCreates a real session, polls completion
pnpm verify:browserOpens real browser, logs in via /login
pnpm verify:browser:sessionOpens a session page, checks live output
pnpm verify:smokeRuns all of the above in sequence
08 · DEPLOY

Ship It

Deployment is two separate things — Orchestrator to Cloudflare, Gateway to VPS. Then six patterns you can steal for your own projects.

Orchestrator → Cloudflare

# 1. Create the D1 auth database wrangler d1 create duraclaw-auth # update database_id in wrangler.toml # 2. Set secrets (encrypted, runtime-only) wrangler secret put CC_GATEWAY_URL wrangler secret put CC_GATEWAY_SECRET wrangler secret put BETTER_AUTH_SECRET wrangler secret put BETTER_AUTH_URL # 3. Run D1 migrations wrangler d1 migrations apply duraclaw-auth # 4. Deploy cd apps/orchestrator && pnpm deploy

Gateway → VPS

1. Install Buncurl -fsSL https://bun.sh/install | bash
2. Clone repo + install depspnpm install in repo root
3. Install systemd service./packages/cc-gateway/systemd/install.sh
4. Configure CF Tunnelcloudflared tunnel → 127.0.0.1:9877 → hostname
5. Set CC_GATEWAY_URL secretwrangler secret put with tunnel hostname
6. Verifypnpm verify:smoke from project root

The Six Patterns You Can Steal

1 · One DO per entity

Per-session, per-user, per-room DOs. Self-contained state machines with private SQLite. No race conditions, built-in persistence.

2 · Agents SDK + useAgent()

Typed, auto-reconnecting WebSocket between browser and DO. No manual connection management.

3 · WebSocket ChatTransport

Custom AI SDK transport over WebSocket instead of HTTP streaming. Works around Worker streaming limitations.

4 · Localhost + CF Tunnel

Bind to 127.0.0.1 only. Use tunnel for external access. Zero open ports. Defense in depth for privileged services.

5 · Human-in-the-loop tools

Intercept PreToolUse, relay to UI, gate on user decision. AI proposes, human disposes.

6 · Verification-driven dev

Real HTTP calls, real browser, real API responses. Not mocks. Especially valuable when AI writes code.

Glossary

TermMeaning
DODurable Object — stateful, single-instance Worker
SessionDOThe DO class that manages one Claude Code session
cc-gatewayBun server on VPS that runs Claude Code
WorkerServerless function on Cloudflare edge
D1Cloudflare serverless SQL (used for auth)
CF TunnelEncrypted outbound tunnel from VPS to Cloudflare
Agents SDKCloudflare's DO + React hook package
Claude Agent SDKAnthropic's programmatic Claude SDK
useAgent()React hook connecting to a DO via WebSocket
WorktreeGit feature: multiple checkouts of same repo
KataDevelopment workflow CLI (not runtime)
EvictionCloudflare shutting down an idle DO
Alarms APILets a DO schedule a wake-up callback
GatewayCommandWorker → VPS message ("do this")
GatewayEventVPS → Worker message ("this happened")
IdempotentRunning twice = running once
State machineDefined states + defined transitions

References

GitHub Repo
codevibesmatter/duraclaw
Durable Objects
Cloudflare docs
Agents SDK
developers.cloudflare.com
Claude Agent SDK
docs.anthropic.com
Cloudflare Tunnel
developers.cloudflare.com
TanStack Start
tanstack.com/start
Vercel AI SDK
sdk.vercel.ai
duraclaw
8 sections
MIT
duraclaw-guide.pages.dev/#home