Skip to content

Isolated Dev Sessions

bun run dev gives you one dev environment pointed at one Convex deployment. That is fine until you want two branches alive at the same time — a feature you are building and a bug you are reproducing, or three agents working in parallel. Both convex dev processes would push schema and functions to the same deployment, and whichever pushed last wins.

bun run worktree:up solves that. It turns a git worktree into a self-contained dev session: its own local Convex backend, its own database, its own Vite port, its own seeded org and accounts. Sessions never see each other.

Terminal window
bun run worktree:up my-feature

That creates the branch my-feature, places its worktree under ~/.worktrees/<repo>/my-feature, installs dependencies, boots a local Convex backend, seeds accounts and an org, and starts Vite. It finishes by printing everything you need:

Session ready
Path: /home/you/.worktrees/red-boilerplate/my-feature
Branch: my-feature
App: http://localhost:5207
Convex: http://127.0.0.1:3210 (anonymous:anonymous-agent)
Credentials: /home/you/.worktrees/red-boilerplate/my-feature/.claude/rules/playwright.local.md

Open the app URL and sign in with the credentials from the seed output. Run it again from inside an existing worktree — with no branch name — to restart that session.

Terminal window
bun run worktree:down

This stops Vite and Convex but keeps the data. The next worktree:up comes back on the same ports with the same org, the same users, and the same passwords.

To throw the data away and provision a clean backend:

Terminal window
bun run worktree:up --reset

To throw the whole session away, remove the worktree: git worktree remove <path>.

Per-session secrets — BETTER_AUTH_SECRET and the ES256 API-key JWT pair — are minted by the script and differ per session, which is what keeps sessions from sharing sign-ins.

Secrets that are genuinely shared (AI gateway, billing, email) live in one file outside every worktree, so git clean -xfd cannot reach it and one copy serves every session:

Terminal window
mkdir -p ~/.red
cp scripts/worktree-secrets.example.sh ~/.red/dev.env

Every key in it is optional, and so is the file. A missing key degrades exactly one service — no AI runs without AI_GATEWAY_API_KEY, no billing without AUTUMN_SECRET_KEY, no outbound email without RESEND_API_KEY. Auth, seeding, and the app itself work regardless. Rotating a key here reaches every session on its next worktree:up.

Set RED_DEV_ENV to keep several sets side by side:

Terminal window
RED_DEV_ENV=~/.red/dev-scratch.env bun run worktree:up

Set DEV_HOST and DEV_SERVE=1 in the secrets file to put tailscale serve in front of a session:

Terminal window
DEV_HOST=my-laptop.tailnet-name.ts.net
DEV_SERVE=1

TLS is the point, not convenience. Better Auth runs with useSecureCookies, and a plain http:// origin on a tailnet name is not a secure context (only localhost is exempt) — the browser would discard the session cookie and crypto.subtle would read as undefined, so sign-in could never complete. tailscale serve terminates TLS with a real certificate and proxies to loopback, so Vite stays bound to localhost and you open no firewall holes.

DEV_HOST without DEV_SERVE binds every interface in the clear instead, and needs the ports opened by hand.

Terminal window
bun run worktree:up --cloud

Provisions a named cloud dev deployment (dev/<branch>, expiring in 7 days) instead of a local backend. Use it when the session needs a public *.convex.site URL — inbound webhooks, OAuth callbacks, an MCP client reaching in from outside your machine. It requires the main checkout to already have a Convex project configured.

The Convex CLI keys local deployments on the project directory, so each worktree automatically gets its own state directory (<worktree>/.convex/local/default), its own sqlite database, its own admin key, and its own port pair.

The load-bearing step is that worktree-up.sh writes a clean root local env file. Copying the main checkout’s would carry CONVEX_DEPLOYMENT=dev:<name>, and every worktree’s convex dev would push to that one shared deployment — exactly the collision the script exists to prevent.

Vite ports come from a hash of the branch slug in the range 5200–5229, so a session keeps a stable URL across restarts and never collides with the bun run dev default of 5173. Convex port pairs are checked against every sibling worktree’s saved claim, not just against what is currently listening — a stopped session still owns its ports, and a sibling that grabbed them would silently attach to the wrong backend.

A local Convex backend compiles "use node" actions with the host’s Node, and Convex builds them only for Node 20, 22, or 24. RED ships none, so this never comes up out of the box. Once you add one, worktree:up checks the host’s Node and switches to a supported version through nvm or mise if one is installed — or tells you to install one, or to use --cloud, which builds node actions server-side.