- Published on
Introducing alliance-platfom-dev
- Authors
- Name
- Dave Coates
Git worktrees are a handy way to have multiple workstreams going at one time but only provide isolation for files.
The rest of the development environment (database, django server, vite server etc) has to be spun up independently.
If the project needs a local .env, the new worktree will not have that either.
Most agent harnesses like Claude Code or Codex support worktrees as a first class concept, so having a new task create a worktree or asking an agent to spin up multiple worktrees in a chat are all easily supported, but having it also get a working environment to verify its changes in isolation is a separate challenge. They often can do this, with some trial and error, but will burn tokens working it out everytime and may not do it consistently or how you might like.
alliance-platform-dev aims to smooth this out. Projects expose it through a checked-in bin/dev command, and every
worktree gets its own database, ports, processes and browser URL.
From another checkout to another environment
The workflow starts with ordinary Git:
git worktree add ../my-project-billing -b feature/billing
cd ../my-project-billing
Instead of manually copying .env, choosing ports and creating a database, start the complete environment with:
bin/dev up
bin/dev url
The runner derives a stable identity from the project and canonical worktree path. It uses that identity to allocate:
- a PostgreSQL database;
- Django and Vite ports;
- a tmux session for Django, Vite and any extra services;
- an optional Portless hostname; and
- state that management commands, tests and Git hooks can use consistently.
The branch name is useful to humans, but it is not the identity. Renaming a branch will not suddenly point it at a different database.
Interactive demo
A two-worktree session, replayed
The main checkout is already running. Watch two more worktrees come up, run a test and get thrown away — while the machine state tracks along.
➜
➜
Your machine
Databases
- acme_dev
Ports
- :8000 django
- :5173 vite
tmux sessions
—
Hostnames
—
Press play to run the whole session, or step through it at your own pace.
The result is that both environments can stay running at the same time. With Portless, each gets a memorable HTTPS hostname such as:
https://billing.my-project.localhost
https://reporting.my-project.localhost
Without Portless they are still isolated and browser-accessible through different localhost ports.
The same command for people and agents
The normal development loop becomes:
bin/dev up
bin/dev status
bin/dev logs django
bin/dev manage shell
bin/dev test billing.tests
bin/dev jstest frontend/src/billing
bin/dev lint
bin/dev restart django
bin/dev down
The normal behaviour of dev servers detecting changes and restarting works as before, bin/dev is just the entry
point that ensures the correct environment (database, server ports, env vars) are used.
These foreground commands receive the same generated database and hostname environment as the running services. A test launched from the reporting worktree cannot quietly fall back to the billing database.
Coding agents use exactly the same interface, with JSON output where it helps:
bin/dev up
bin/dev status --json
bin/dev url --json
# implement and verify the task
bin/dev test billing.tests
bin/dev check
bin/dev down --drop-db --yes
This gives an agent a bounded environment it can create, inspect and clean up without coordinating port numbers with a developer—or with another agent running beside it.
Fast databases instead of repeated setup
The default setup will create a database and run migrations and createdevdata. On a mature project, generating
realistic development data can take much longer than creating the worktree itself.
You can configure a prepared PostgreSQL database as a template:
database_template = "my_project_dev_template"
The first bin/dev up in a worktree clones that database, runs current migrations and skips the expensive createdevdata step.
Later starts reuse the worktree database.
For large local databases, PostgreSQL's FILE_COPY strategy can be much faster:
database_template = "my_project_dev_template"
database_template_strategy = "file_copy"
Sometimes cloned data needs one worktree-specific adjustment. For example, an organisation record may need an HTTP host matching the Portless hostname for application routing. A project-owned management command can handle that once, immediately after creation:
db_prepare_command = ["prepare_worktree_db"]
The command receives the generated database environment and DEV_BASE_HOST, letting project-specific setup remain in
the project rather than in the runner.
One private environment configuration for all worktrees
If you have environment variables that need to be available for all worktrees currently you would copy the .env file
to each created worktree. With bin/dev each project has a private user-level configuration outside its checkouts:
bin/dev config edit global
Values under an environment table apply to every worktree for that project:
[environment]
DB_HOST = "localhost"
EXTERNAL_API_TOKEN = "..."
The file lives under ~/.config/alliance/dev/<project-id>/config.toml and is created with private permissions. A worktree can still override it in .dev-server/config.toml, and the invoking shell has final precedence.
Generated values such as DB_NAME remain owned by the runner, so setting these will do nothing - see the Environment table for a full list.
Deliberately project-owned at the edges
The runner manages lifecycle and isolation, but it does not try to understand every project's Definition of Done. Test, frontend-test, lint and full-check commands are configured as argument arrays and delegated to project-owned scripts.
That means an existing project can keep commands such as bin/run-tests-django.sh and bin/lint.sh. Python-backed verification commands automatically use the project's .venv when the caller has not activated one, so wrappers can invoke tools such as coverage directly.
Projects can also add long-running services alongside Django and Vite:
[[extra_processes]]
name = "worker"
command = ["uv", "run", "python", "manage.py", "run_worker"]
cwd = "django-root"
required = true
Cleanup
bin/dev down stops processes but retains the database, making the next start quick. For a disposable worktree:
bin/dev down --drop-db --yes
The runner also keeps a private machine-wide registry. If an agent or developer removes a worktree before cleaning it up, another worktree from the same project can still find the retained resources:
bin/dev env list
bin/dev env remove <worktree-id> --yes
Adding it to an existing project
Install from the project root:
uvx --from alliance-platform-dev alliance-dev install
The installer creates and configures the checked-in bin/dev launcher and config/dev.toml. It discovers common
Django, Vitest, lint and check entry points, and can update Husky pre-commit and pre-push hooks so they use the current worktree environment.
You can copy some configuration scripts, or the lint runners, from the template project
if useful. This will depend on the age of the project and how much it has diverged, but copying the full lint setup
and the new ./bin/ scripts added in this MR is one option.
After reviewing the generated configuration:
uv sync
bin/dev doctor
bin/dev up
The application does not take a runtime dependency on the tool. The launcher runs a pinned package release in an isolated environment, while project commands continue to use the project's own dependencies.
See the Alliance Platform Dev documentation for configuration, command and installation details.
Do I have to use it?
No - everything will work as before, but it's a useful tool for managing multiple streams of work so consider it for those situations.