Ditto is a TanStack Start app deployed with Alchemy on Cloudflare Workers. It uses:
- Cloudflare D1 + Drizzle for persistence
- better-auth with GitHub OAuth
- GitHub repo import for projects
@cloudflare/sandboxfor workspace instantiation- R2-backed sandbox backup/restore
- agent runs in the Cloudflare sandbox via the PI harness; the client streams
POST /api/agent/stream(SSE)
| Path | Ownership |
|---|---|
Root (package.json, alchemy.run.ts, Dockerfile, pnpm-workspace.yaml) |
Workspace orchestration, Alchemy deploy graph, sandbox image |
apps/web |
TanStack Start application (@ditto/web): UI, Worker routes, domain services, D1 schema/migrations |
packages/sandbox-runner |
Independent npm package baked into the sandbox image (not a pnpm workspace member) |
Alchemy is the sole deployment owner (pnpm dev / pnpm deploy / pnpm destroy). This monorepo layout does not introduce SST, Wrangler-as-deploy, or any other deployment boundary.
- Node.js 22.15+ for the app (22.19+ for the sandbox runner; 22.17+ recommended for local dev)
- pnpm (workspace root +
apps/web) and npm (packages/sandbox-runneronly) - Cloudflare / GitHub credentials for the configured environment
Root workspace (pnpm; includes apps/web):
pnpm install --frozen-lockfileSandbox runner is a separate npm package (not a pnpm workspace member). Install it when working on packages/sandbox-runner or before full verification:
npm ci --prefix packages/sandbox-runnerOr: pnpm runner:install.
From the repository root:
pnpm devAlchemy runs from the root, generates local Wrangler config under
apps/web/.alchemy/local/, and starts Vite with apps/web as cwd (env files
resolve from the repo root via envDir).
Before opening a PR, run the full verification gate (app + runner typecheck/tests/build):
pnpm verifyWhen you change packages/sandbox-runner sources, its package.json / lockfile,
or the root Dockerfile, rebuild the sandbox image so the Docker-baked runner
matches local code (restart pnpm dev or redeploy with Alchemy so the root
Dockerfile is rebuilt).
Generate migrations after changing apps/web/src/db/schema.ts:
pnpm db:generateMigrations live under apps/web/migrations. Root pnpm db:* scripts forward to
@ditto/web.
pnpm dev— local Alchemy + Vite (root)pnpm build— production build of@ditto/webpnpm deploy— deploy with Alchemy (sole deploy owner)pnpm destroy— tear down Alchemy resourcespnpm check— Biome check (repo root)pnpm lint— Biome lintpnpm format— Biome formatpnpm test— Vitest for@ditto/webpnpm typecheck— TypeScript for@ditto/web(tsc --noEmit)pnpm runner:install—npm ciforpackages/sandbox-runnerpnpm runner:verify— typecheck, test, and build the sandbox runnerpnpm verify— full pre-PR gate (check, typecheck, test, build, runner verify)pnpm db:generate/db:migrate/db:push/db:pull/db:studio— Drizzle via@ditto/web
The root Dockerfile copies packages/sandbox-runner into the Cloudflare sandbox
image and installs the ditto-runner CLI. Build context is the repository root
(see .dockerignore).
Set these for local development and deployment (typically .env.local at the
repo root; Vite envDir is the monorepo root):
CLOUDFLARE_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
USE_LOCAL_BUCKET_BACKUPS=
BETTER_AUTH_SECRET=
BETTER_AUTH_URL=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY=
VITE_GITHUB_APP_INSTALL_URL=
OPENCODE_API_KEY=BETTER_AUTH_URL defaults to http://localhost:5173 if omitted.
- For GitHub-linked projects, the Ditto GitHub App needs Contents: Read & write and Pull requests: Read & write so the Worker can push session branches and open pull requests (installation token; never stored in the DB).
pnpm deployandpnpm destroyare managed through Alchemy only.apps/web/src/server.tsexports the Cloudflare Sandbox binding used by the app.OPENCODE_API_KEYis required for sandbox agent runs (passed into the harness session environment).- Agent harness architecture:
docs/architecture/agent-harness.md - Concurrent agent runs per project are not enforced yet; see the architecture doc for deferred concurrency notes.