Miles Roxas Logo

Brooklyn, NY

Fourteen branches, one afternoon: how I work in parallel as a solo engineer

Published
By
Miles Roxas

The bottleneck was never typing

On October 7 this site's repo took 58 commits. They came from 14 workspaces I opened that day, each on its own branch, each with its own dev server and database, most of them driven by a coding agent while I reviewed the one before. Since 25 September the count is 170 commits, 23 migrations and 37 workspace branches, from one person.

None of that is possible with one checkout. A solo engineer is rarely blocked by how fast they can write code. They are blocked by waiting: a dev server restarting, an agent running for ten minutes, a schema push that has to finish before the next thing starts. One checkout means one branch, one port, one database. Start a second task and you stash, switch, re-sync the schema and lose the state of the first.

The fix is not a faster machine. It is making a task cheap to open and cheap to throw away, then running as many at once as attention allows. The rest of this post is the setup that does that: the worktree tooling, the database isolation, and the migration gates that keep parallel schema work from breaking production.

Commits per day since Conductor went in

Commits

Day

Daily commits on the dev branch. The workspace tooling landed on 3 October; the spike is 14 workspaces landing on the same afternoon.

Source: git log, milesroxas/milesroxas-site

Description and data table

A bar chart of commits per day from 25 September to 12 October 2026. Before 3 October the days run between 1 and 12 commits. After the Conductor workspace setup on 3 October the days run between 6 and 16, with one day, 7 October, at 58 commits. The takeaway is that parallel workspaces raised the steady daily rate and made a 58-commit day possible for one person.

Commits per day since Conductor went in
DayCommits on dev
Sep 25, 20261
Sep 26, 20262
Sep 27, 202612
Sep 28, 20262
Sep 29, 20264
Sep 30, 202610
Oct 1, 20268
Oct 2, 20262
Oct 3, 20261
Oct 4, 202614
Oct 5, 20266
Oct 6, 202616
Oct 7, 202658
Oct 8, 20268
Oct 9, 20267
Oct 10, 20266
Oct 11, 202610
Oct 12, 20263
The GitHub commit list for the dev branch on 7 October 2026: eleven of the days 58 commits, each scoped to one area such as works, carousel-tabs or chrome, each landed from its own Conductor workspace.

One day on dev: 58 commits from 14 workspaces, each one scoped to a single area.

A workspace is a whole site

Conductor opens a git worktree per task and runs a coding agent inside it. That is the easy half. The repo's job is to make each worktree a complete, isolated copy of the site: its own port, its own Postgres database, its own .env. Three small scripts do it, and a fourth holds the shared helpers.

  • setup.sh runs once when the workspace is created. It copies .env from the main checkout, rewrites POSTGRES_URL and NEXT_PUBLIC_SERVER_URL for this workspace, runs pnpm install, and creates the database.
  • run-dev.sh is the Run button. It re-checks Postgres and the database, then starts Next on $CONDUCTOR_PORT.
  • archive.sh drops the workspace database and nothing else.
  • lib.sh is sourced by all three and owns the database identity, the port checks and the guards.

What a workspace gets, from open to archive

.envMain checkoutWorktreeShared Postgres, port 54330New task.env and the payloadDBConductor opens aworktreesetup.sh: copy .env,set port and DB URLpnpm installrun-dev.sh on$CONDUCTOR_PORTrebase, push HEAD:devclone payload intopayload_cityarchive.sh dropspayload_city

Only the main checkout's .env and the shared Postgres container are touched from outside the worktree. Everything else lives and dies with it.

Description and steps

A left to right flow. A new task makes Conductor open a worktree. setup.sh copies .env from the main checkout, sets the workspace port and database URL, runs pnpm install, and clones the main dev database payload into payload_city inside the one shared Postgres container. run-dev.sh then serves the site on the workspace port. Work lands by rebasing onto origin/dev and pushing HEAD to dev. Archiving drops the workspace database. The emphasised node is the running dev server.

  1. New task (start or end)
  2. .env and the payload DB (Main checkout)
  3. Conductor opens a worktree (Worktree)
  4. setup.sh: copy .env, set port and DB URL (Worktree)
  5. pnpm install (Worktree)
  6. clone payload into payload_city (Shared Postgres, port 54330)
  7. run-dev.sh on $CONDUCTOR_PORT (Worktree)
  8. rebase, push HEAD:dev (Worktree)
  9. archive.sh drops payload_city (start or end, Shared Postgres, port 54330)

Connections

  • New task to Conductor opens a worktree
  • Conductor opens a worktree to setup.sh: copy .env, set port and DB URL
  • .env and the payload DB to setup.sh: copy .env, set port and DB URL: .env (optional or async)
  • setup.sh: copy .env, set port and DB URL to pnpm install
  • setup.sh: copy .env, set port and DB URL to clone payload into payload_city
  • pnpm install to run-dev.sh on $CONDUCTOR_PORT
  • clone payload into payload_city to run-dev.sh on $CONDUCTOR_PORT
  • run-dev.sh on $CONDUCTOR_PORT to rebase, push HEAD:dev
  • rebase, push HEAD:dev to archive.sh drops payload_city

Conductor reads one file, .conductor/settings.toml. It names the scripts, tells Conductor that workspaces may run at the same time, and gives every agent that opens in this repo the landing rule, so Claude and Codex both get it without reading CLAUDE.md.

bash
# .conductor/settings.toml
file_include_globs = ".env" # not .env*: that would copy production credentials
[scripts]
setup = "bash .conductor/setup.sh"
archive = "bash .conductor/archive.sh"
run_mode = "concurrent" # own port + own DB, so any number can run at once
[scripts.run.dev]
command = "bash .conductor/run-dev.sh"
default = true
available_in = ["local"]
[prompts]
general = """Land work on `dev`, not `main`: rebase onto `origin/dev`,
then `git push origin HEAD:dev`. Never push to `main`."""
[[preview_urls]]
name = "Site"
url = "http://localhost:$CONDUCTOR_PORT"

The database is the part most parallel setups get wrong. Running one Postgres per workspace is slow and eats memory; sharing one database between branches is worse, because two branches pushing different schemas into it fight on every restart. The middle path is one container, one database per workspace.

docker-compose.yml pins the compose project name, so every checkout addresses the same container and volume whatever directory it runs from. The image is the pgvector build of Postgres 17, the same major as Neon in production, so what push does locally matches what the migration does there.

bash
# docker-compose.yml
name: milesroxas # fixed: every checkout targets the same container
services:
postgres:
image: pgvector/pgvector:pg17-trixie # same major as Neon production
restart: unless-stopped
ports:
- '54330:5432' # this repo's port; sas-site owns 54320
environment:
POSTGRES_USER: postgres
POSTGRES_DB: payload
POSTGRES_HOST_AUTH_METHOD: trust
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 5s

A new workspace database is a clone of the main dev database, payload, which is itself a production restore pulled by the dev TUI. So every workspace opens with real content, offline, and the clone takes about a second: CREATE DATABASE ... TEMPLATE copies files rather than rows. Postgres refuses the template while another session is connected to it, which is whenever the main dev server is running, so the helper falls back to a dump and restore.

bash
# .conductor/lib.sh
clone_local_db() {
if psql_c -c "CREATE DATABASE \"$DB_NAME\" TEMPLATE \"$MAIN_DB\"" 2>/dev/null; then
return 0 # ~1 s: file copy, no other session on 'payload'
fi
psql_c -c "CREATE DATABASE \"$DB_NAME\""
compose exec -T postgres pg_dump -U postgres --no-owner --no-acl "$MAIN_DB" |
psql_c -d "$DB_NAME" >/dev/null # ~30 s when the main dev server is up
}
# Every DROP in these scripts goes through this.
assert_workspace_db_name() {
case "$1" in
payload_?*) ;;
*) echo "conductor: refusing to touch database '$1'" >&2; return 1 ;;
esac
}

The scripts are short. The guards around them are where the time went, and each one is a bug I hit once.

  • Database identity is the directory, not the name. Conductor renames a workspace to its branch after the first chat. Keyed on the name, the next Run created a second database and the archive dropped the wrong one. Orphans piled up at 50 MB each. Now the DB name comes from the worktree directory, and once .env exists its POSTGRES_URL is the source of truth.
  • Ports are checked before use. Conductor reserves ten ports per workspace but never checks they are free. A dev server orphaned from another repo can still hold one, and the Open button would reach the wrong site. run-dev.sh refuses to start on a taken port and prints the owning pid and checkout.
  • The process env is pinned. The Run button can hand the script an environment that already carries the root checkout's POSTGRES_URL, and Next.js prefers process env over .env. The dev server would then write to the shared database while the banner claimed the workspace one. lib.sh exports both URLs explicitly.
  • .env.local is refused. vercel env pull writes it with the production database URL, and Next.js loads it over .env. Push would then offer to drop production tables. Setup and Run stop if any override file names a different database.
  • Prune only drops what setup made. Each workspace DB carries a database comment, and prune-dbs.sh only considers marked databases, so a hand-made worktree's database is never taken for an orphan.

Push in dev, migrations in CI

Two workspaces changing the schema at once is the real test of a parallel setup. The rule that makes it safe has two halves. Local databases are push-managed: on every pnpm dev, Drizzle diffs the Payload config against the workspace database and applies the difference. Neon, in preview and production, is only ever changed by payload migrate inside the Vercel build. Nothing runs migrations by hand, and push is wired so it cannot reach a remote database.

typescript
// src/payload.config.ts
/** Drizzle push only ever targets the local Docker DB, never Neon. */
const isLocalDatabase = (url: string | undefined): boolean => {
try {
const { hostname } = new URL(url ?? '')
return hostname === '127.0.0.1' || hostname === 'localhost'
} catch {
return false
}
}
// Push in local dev, migrations in CI. Payload forbids mixing the two on
// one database, so push is limited to a local URL.
push: process.env.PAYLOAD_DB_PUSH !== 'false' && isLocalDatabase(process.env.POSTGRES_URL),

The catch is in how migrations are generated. payload migrate:create never looks at a database. It diffs the config against the newest snapshot file in src/migrations. That gives one rule for parallel branches: a migration's snapshot must be generated on top of the newest migration on dev at the moment it lands there.

Break it and the failure is delayed and ugly. Workspace A and workspace B both branch from dev, whose newest snapshot is S0. A generates a migration with snapshot S0 plus A's columns; B generates one with S0 plus B's. Both apply cleanly in CI. After both land, the newest snapshot is whichever merged last, and it does not know about the other's columns. The next migrate:create on any branch re-emits them, and the production build dies on column already exists.

So the rule is enforced three times, because each check sees something the others cannot.

A schema change on its way to production

yesnonoyesChange a collection orfieldpnpm dev: push syncsthe workspace DBmigrate:create diffsconfig vs newestsnapshotgit rebase origin/devdev gained amigration?delete and regeneratethe migrationpre-push hook:migration present, inorder, no drift?fix, push againlands on devGitHub re-checks thesnapshot chainMiles pushes dev tomainVercel: guard, payloadmigrate, next build

The pre-push hook sees the branch; the GitHub check sees the merge result; the Vercel build is the only thing that ever writes to Neon.

Description and steps

A left to right flow for a schema change. Editing a collection or field leads to pnpm dev, where Drizzle push syncs the workspace database. Then migrate:create diffs the config against the newest snapshot and writes a migration file. Before landing, the branch rebases onto origin/dev. A decision asks whether dev gained a migration in the meantime; if yes, the branch's migration is deleted and regenerated on top of it. The pre-push hook then checks three things: schema files changed without a migration, the branch's migrations sort after dev's newest, and the newest snapshot matches the config. If it fails, fix and push again. If it passes, the work lands on dev and a GitHub workflow repeats the snapshot check on the merged result. A release is a fast-forward push from dev to main, which triggers the Vercel build: a guard confirms the preview database is not production, payload migrate applies the file, then next build runs. The emphasised node is the Vercel build.

  1. Change a collection or field (start or end)
  2. pnpm dev: push syncs the workspace DB
  3. migrate:create diffs config vs newest snapshot
  4. git rebase origin/dev
  5. dev gained a migration? (decision)
  6. delete and regenerate the migration
  7. pre-push hook: migration present, in order, no drift? (decision)
  8. fix, push again
  9. lands on dev
  10. GitHub re-checks the snapshot chain
  11. Miles pushes dev to main (start or end)
  12. Vercel: guard, payload migrate, next build

Connections

  • Change a collection or field to pnpm dev: push syncs the workspace DB
  • pnpm dev: push syncs the workspace DB to migrate:create diffs config vs newest snapshot
  • migrate:create diffs config vs newest snapshot to git rebase origin/dev
  • git rebase origin/dev to dev gained a migration?
  • dev gained a migration? to delete and regenerate the migration: yes
  • delete and regenerate the migration to pre-push hook: migration present, in order, no drift?
  • dev gained a migration? to pre-push hook: migration present, in order, no drift?: no
  • pre-push hook: migration present, in order, no drift? to fix, push again: no
  • fix, push again to pre-push hook: migration present, in order, no drift? (optional or async)
  • pre-push hook: migration present, in order, no drift? to lands on dev: yes
  • lands on dev to GitHub re-checks the snapshot chain
  • GitHub re-checks the snapshot chain to Miles pushes dev to main
  • Miles pushes dev to main to Vercel: guard, payload migrate, next build

The pre-push hook is the one that earns its keep. It is an allowlist of schema-owning files, not whole folders, so a change to a block's component or a story never trips it and nobody learns to bypass it. When a push adds a migration, it fetches origin/dev and fails unless the branch holds every migration on dev and its own sort after dev's newest. When schema files changed, it runs the same diff migrate:create would, with no database and no files written, so a field added after the migration was generated is caught before it leaves the machine.

The other side is kept deliberately dull. Only main deploys; a push to dev builds nothing, which saves build minutes and means a release is one fast-forward push that only I make. The build command is three steps, and the first one exists because a preview build once pointed at the wrong database.

json
// vercel.json
{
"buildCommand": "pnpm run ci",
"ignoreCommand": "bash scripts/vercel-ignore-build.sh",
"git": { "deploymentEnabled": { "*": false, "main": true, "dev": false } }
}
// package.json
"ci": "pnpm guard:preview-db && payload migrate && pnpm build"

A day in the loop

A working day looks like this. I open a workspace per task and give each one a line: fix the carousel tab position, make capabilities a taxonomy, tune the mask animation speed. Setup runs on its own. The agent builds while I review the previous workspace's preview on its own port, with real content, and Storybook one port up when a component needs isolating. When a task is right, it lands with a rebase and a push to dev, and the workspace is archived. Three to six are open at any time; the limit is my attention, not the machine.

One task, from a line of text to production

MilesWorkspaceand agentGitHubVercelopen with a one-line taskenv, deps, DB cloneagent builds, Miles checks the previewrebase, push HEAD:devsnapshot-chain checkarchive: DB droppedrelease: push dev to mainbuild mainguard, migrate, buildproduction live

The workspace does the setup and the landing. I do the review and the release.

Description and messages

A sequence between Miles, a workspace with its agent, GitHub, and Vercel. Miles opens a workspace with a one-line task. The workspace sets itself up: env, dependencies, a database clone. The agent builds and Miles checks the preview on the workspace port. The workspace rebases and pushes HEAD to dev; GitHub runs the snapshot-chain check. Miles archives the workspace, which drops its database. Later Miles pushes dev to main. GitHub triggers the Vercel build, which runs the database guard, payload migrate and next build, and production is live.

  1. Miles to Workspace and agent: open with a one-line task
  2. Workspace and agent, to itself: env, deps, DB clone
  3. Workspace and agent, to itself: agent builds, Miles checks the preview
  4. Workspace and agent to GitHub: rebase, push HEAD:dev
  5. GitHub, to itself: snapshot-chain check
  6. Miles to Workspace and agent: archive: DB dropped
  7. Miles to GitHub: release: push dev to main
  8. GitHub to Vercel: build main
  9. Vercel, to itself: guard, migrate, build
  10. Vercel to Miles: production live (reply)

Parallel only pays off when the tasks are independent, so the commit scopes tell the story better than the count. Since 25 September the work spread across the case-study dial, the scroll reveal system, media on Cloudflare, the composer blocks, posts, site chrome and more, and most of those streams were open on the same days.

Where the work went since 25 September

Scope

Commits

Commits by conventional-commit scope. Only scopes with four or more commits are shown.

Source: git log, milesroxas/milesroxas-site

Description and data table

A horizontal bar chart of commits by scope since 25 September 2026. works has 36, reveal 13, media 13, composer 13, posts 10, chrome 10, blocks 8, carousel 5, figures 4 and cursor 4. The takeaway is that the commits were spread across ten independent areas rather than one feature, which is what parallel workspaces are for.

Where the work went since 25 September
ScopeCommits
works36
reveal13
media13
composer13
posts10
chrome10
blocks8
carousel5
figures4
cursor4

Content runs through the same loop. This site exposes an MCP server, so an agent in one workspace can draft a post or patch a block against the live CMS while another workspace is changing code. This post was drafted that way, with the charts above built from the repo's own git log.

What it costs, so the picture is honest:

  • Each workspace database is about 48 MB. Archiving drops it, and prune-dbs.sh reclaims any that an offline Docker left behind.
  • Setup takes about thirty seconds instead of one when the main dev server is connected to payload, because Postgres refuses the template.
  • A push to dev deploys nothing. A preview of dev is a manual deployment from the Vercel dashboard, which is one more click than a PR-per-branch flow gives you.
  • The guards took longer to write than the scripts, and each one was written after the bug, not before.

The point is not the commit count. It is that a half-finished thought never blocks the next one. A task that would have waited for a free checkout now waits for nothing, and the schema rules mean I can let several of them touch the database at once and still trust the release.

By the numbers

  1. 170 commits

    On dev since 25 September 2026, with 23 migrations among them.

    01
  2. 37 workspaces

    Opened between 3 and 12 October, 14 of them on 7 October.

    02
  3. About 1 second

    To clone a workspace database from the main dev DB with CREATE DATABASE TEMPLATE.

    03
  4. 3 gates

    Pre-push hook, GitHub check on the merge, and the Vercel build as the only writer to Neon.

    04

More posts

All posts