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.
- Exact deps:
alchemy@2.0.0-beta.70,effect@4.0.0-beta.103,@effect/platform-node@4.0.0-beta.103(workspace overrides pin the coherent Effect beta.103 set). - Container id
sandbox, envSandbox; image from rootDockerfile. pnpm devruns Alchemy fromapps/web(entry../../alchemy.run.ts) so local Docker context resolves to the repo root.pnpm deploy/pnpm destroystay root-level--stage dev.- Local state:
.alchemy/(gitignored). Stage is disposable.
- 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 devRuns Alchemy from apps/web against stage dev so the root Docker context
resolves correctly. Env files load from the repo root.
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 for stagedevonlypnpm build— production build of@ditto/webpnpm deploy—alchemy deploy --stage dev(sole deploy owner)pnpm destroy—alchemy destroy --stage dev(destructive; no continuity)pnpm 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=
AI_CREDENTIALS_ENCRYPTION_KEY=BETTER_AUTH_URL defaults to http://localhost:5173 if omitted.
AI_CREDENTIALS_ENCRYPTION_KEY must be nonempty and distinct from
BETTER_AUTH_SECRET. It encrypts account-level AI provider credentials.
OPENCODE_API_KEY is the operator credential for the free fallback model
opencode/deepseek-v4-flash-free only. Users connect their own providers in
Account Settings.
- 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, stagedevonly.apps/web/src/server.tsexports the Cloudflare Sandbox binding used by the app.OPENCODE_API_KEYis required as the operator fallback foropencode/deepseek-v4-flash-free. Account provider credentials are injected ephemerally viaDITTO_PI_CREDENTIALand deleted inside the runner before tools start.- 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.