From zero to deployed in 4 steps. You need accounts on three services, one config value, and one command.
Install these tools locally. Commands below are for macOS with Homebrew -- adjust for your platform.
# Node.js 22+
brew install node@22
# pnpm
brew install pnpm
# uv (Python package manager, used for Modal backend)
brew install uv
# Bun
brew install oven-sh/bun/bun
# Wrangler CLI (Cloudflare)
npm install -g wrangler
# Modal CLI
uv tool install modal
# jq (used by deploy script)
brew install jqYou need three accounts:
| Service | What it does | Sign up |
|---|---|---|
| Cloudflare | Hosts the API (Worker), database (D1), storage (R2), and frontend (Pages) | dash.cloudflare.com |
| Modal | Runs sandbox containers (the coding environments) | modal.com |
| GitHub | OAuth login for users + repo access inside sandboxes | github.com/settings/developers |
Cloudflare -- Workers paid plan required for Durable Objects. R2 must be enabled in the Cloudflare dashboard (Workers & Pages > R2) before deploying.
GitHub OAuth App -- Create one at Settings > Developer settings > OAuth Apps:
| Field | Value |
|---|---|
| Homepage URL | https://your-domain.com (or http://localhost:5173 for dev) |
| Callback URL | https://<PROJECT_NAME>.<your-subdomain>.workers.dev/auth/github/callback |
Save the Client ID and Client Secret.
Optional: Google OAuth for Google sign-in and integrations.
wrangler login
modal token setcp .env.deploy.example .env.deployEdit .env.deploy and set one value:
PROJECT_NAME=valet-yournameAll Cloudflare resource names derive from this automatically:
| Resource | Name |
|---|---|
| Worker | valet-yourname |
| Pages | valet-yourname-client |
| D1 | valet-yourname-db |
| R2 | valet-yourname-storage |
pnpm install
make deployThis automatically:
- Creates D1 database and R2 bucket if they don't exist
- Discovers your Modal workspace from the CLI
- Deploys the Cloudflare Worker (API + Durable Objects)
- Runs D1 migrations
- Deploys the Modal backend (sandbox orchestration)
- Builds the frontend with the correct Worker URL and deploys to Cloudflare Pages
After the first deploy, set worker secrets:
wrangler secret put ENCRYPTION_KEY --name valet-yourname # Any string, 32+ chars
wrangler secret put GITHUB_CLIENT_ID --name valet-yourname # From your GitHub OAuth app
wrangler secret put GITHUB_CLIENT_SECRET --name valet-yourname # From your GitHub OAuth app
wrangler secret put FRONTEND_URL --name valet-yourname # e.g. https://valet-yourname-client.pages.devVisit https://valet-yourname-client.pages.dev and sign in with GitHub.
For incremental updates after the initial deploy:
make deploy-worker # Cloudflare Worker only
make deploy-modal # Modal backend only (sandbox orchestration)
make deploy-client # Frontend only (builds + deploys to Pages)
make deploy # Full deploy (auto-discovers everything)There's also make release, which runs a more comprehensive pipeline: install, typecheck, build and push the OpenCode Docker image to GHCR, deploy Worker, run D1 migrations, and deploy Pages.
Sandbox images are built and cached by Modal (defined in backend/images/base.py). To force a rebuild after changing docker/ or packages/runner/:
- Bump
IMAGE_BUILD_VERSIONinbackend/images/base.py - Redeploy:
make deploy-modal - New sessions will use the updated image (existing sandboxes are not affected)
| What | Where |
|---|---|
| Worker URL | https://<PROJECT_NAME>.<subdomain>.workers.dev |
| Frontend URL | https://<PROJECT_NAME>-client.pages.dev |
| Modal dashboard | https://modal.com/apps/<workspace>/main/deployed |
| D1 console | Cloudflare dashboard > Workers & Pages > D1 |
| Worker logs | wrangler tail (from packages/worker/) |
| Worker secrets | wrangler secret list (from packages/worker/) |