Skip to content

Adding devflow to an existing project

Terminal window
cd ~/existing-project
devflow init

init never touches your application code. It writes .devflow.yml, installs VCS hooks (post-checkout and pre-commit; both marked so devflow uninstall-hooks removes only devflow’s), and registers the project in local state.

For non-interactive adoption, start from a small committed config and validate it:

Terminal window
devflow config-validate
devflow install-hooks
devflow capabilities

Materialize a Git ref that existed before devflow with switch:

Terminal window
devflow switch feature/auth

Worktrees you created manually with git worktree add are picked up automatically: the installed post-checkout hook detects worktree context and runs the setup (file copying + service workspace creation). To do it explicitly from inside a worktree:

Terminal window
devflow worktree-setup
devflow link feature/auth --from main # record immutable provenance + provision services

Most existing applications have three kinds of Compose services. Move them at different speeds:

Existing Compose itemGood devflow targetWhy
PostgreSQL/MySQL/ClickHouseservices: with type: shared or type: localWorkspace-specific data and connection URLs.
Redis/cache/object storageservice_type: redis or service_type: rustfsOne shared engine, per-workspace DB index/bucket.
App server, frontend dev server, workers, schedulersprocesses.daemons with provider: nativeRuns in each worktree with service env injected.
Search/mail/queue containers you still want as containersservice_type: generic, or keep them in Compose temporarilyHybrid migration without a big-bang rewrite.
env_file values that depend on ports/brancheshooks.post-switch write-envRegenerated on every switch with the right workspace URLs.

A practical migration is: move the database first, generate .env.local, then move app processes once the app can run on the host.

This replaces Compose-managed Postgres/Redis containers with devflow-managed shared engines. Each workspace gets its own Postgres database and Redis DB index, while the engine containers stay global and cheap.

services:
- name: app-db
type: shared
service_type: postgres
default: true
shared:
image: postgres:17
port: 5432
template_branching: true
- name: cache
type: shared
service_type: redis
shared:
image: redis:7
worktree:
path_template: "../{repo}.{workspace}"
copy_files: [.env]
hooks:
post-switch:
env:
action:
type: write-env
path: .env.local
vars:
DATABASE_URL: "{{ service['app-db'].url }}"
REDIS_URL: "{{ service['cache'].url }}"
# Framework-specific aliases are fine too:
CACHE_URL: "{{ service['cache'].url }}"
CELERY_BROKER_URL: "{{ service['cache'].url }}"

Bootstrap and seed the configured default database:

Terminal window
devflow service up
DEFAULT_WORKSPACE="$(devflow --json list | jq -r '.default_workspace')"
devflow service create "$DEFAULT_WORKSPACE"
DEFAULT_DATABASE_URL="$(devflow connection "$DEFAULT_WORKSPACE" --format uri)"
# Example: import from an existing Compose Postgres container.
docker compose exec -T postgres pg_dump -U postgres postgres | psql "$DEFAULT_DATABASE_URL"

Now create a workspace. With template_branching: true, workspace databases are copied from the parent database:

Terminal window
devflow switch -c feature/auth
devflow connection feature/auth --format env

If you prefer one full Docker container per workspace instead, use local containers for Postgres:

services:
- name: app-db
type: local
service_type: postgres
default: true
local:
image: postgres:17

Once the app can run from the host (for example through mise, uv, npm, bun, pdm, or your language’s task runner), move Compose app containers to devflow processes:

processes:
provider: native # or omit for native
auto_start: true
auto_stop: true
daemons:
web:
run: "mise x -- python manage.py runserver 127.0.0.1:$PORT"
port: { expect: [8000], bump: 100 }
ready_port: 8000
env:
DATABASE_URL: "{{ service['app-db'].url }}"
REDIS_URL: "{{ service['cache'].url }}"
worker:
run: "mise x -- celery -A app worker -l INFO"
required: false
env:
DATABASE_URL: "{{ service['app-db'].url }}"
REDIS_URL: "{{ service['cache'].url }}"
scheduler:
run: "mise x -- celery -A app beat -l INFO"
required: false
depends: [worker]
env:
DATABASE_URL: "{{ service['app-db'].url }}"
REDIS_URL: "{{ service['cache'].url }}"

$PORT is set by devflow after port bumping, and ready_port: 8000 follows the bumped port. The native supervisor is included in devflow and records process state for the CLI and desktop.

Useful commands:

Terminal window
devflow process start --all
devflow process status
devflow process logs web --tail 100 --follow
devflow process stop --all
devflow daemon start # keep shared engines alive; reconcile watch/retry

See Project processes for readiness checks, watch/retry, logs, proxy URLs, and approval behavior.

You do not need to migrate everything at once.

Generate .env.local from devflow, but keep running your app container manually. Change the app container env to point at the devflow URLs instead of Compose hostnames. This is useful when the app still depends on container-only tooling.

2. App processes in devflow, a few containers left in Compose

Section titled “2. App processes in devflow, a few containers left in Compose”

Move the web/worker/scheduler commands to processes.daemons, but keep specialized containers (mailcatcher, search, queue emulator, browser test grid) in Compose until you have a reason to move them.

3. Devflow generic containers for non-branching dependencies

Section titled “3. Devflow generic containers for non-branching dependencies”

For a dependency that should remain one shared local container for all workspaces:

services:
- name: search
type: local
service_type: generic
auto_workspace: false
generic:
image: opensearchproject/opensearch:2
port_mapping: "9200:9200"
environment:
discovery.type: single-node

For a dependency that needs one container per workspace, keep auto_workspace: true and use port_range_start rather than a fixed port mapping.

If a team already uses a hosted branching database, use a cloud provider (neon, dblab, xata) or keep the external URL in .devflow.local.yml. Avoid committing personal credentials; commit only the shared shape and keep secrets machine-local.

git:
auto_create_on_workspace: true # provision services for manually added worktrees
main_workspace: main
workspace_filter_regex: "^(feature|fix|agent)/.*" # only these patterns
exclude_workspaces: [main, master, develop] # never these

Per-machine overrides go in .devflow.local.yml (gitignored), quick toggles in environment variables — e.g. DEVFLOW_DISABLED=true to turn devflow off entirely, or DEVFLOW_CURRENT_BRANCH_DISABLED=true for just the branch you’re on.

If your project uses mise, pair it with devflow hooks so new worktrees are immediately trusted and tooled:

hooks:
post-create:
mise-trust:
command: "mise trust --quiet || true"
condition: "file_exists:mise.toml"
mise-install:
command: "mise install"
condition: "file_exists:mise.toml"
continue_on_error: true

Or install the recipe, which detects which of mise / direnv / .env.example actually apply:

Terminal window
devflow hook install workspace-setup

In non-interactive automation, approve trusted hook/process command templates once:

Terminal window
devflow hook approvals add "mise trust --quiet || true"
devflow hook approvals add "mise install"
devflow hook approvals add "mise x -- python manage.py runserver 127.0.0.1:$PORT"
Terminal window
devflow config-validate
devflow hook vars --workspace main
devflow service status
devflow connection main --format env
devflow switch -c feature/devflow-smoke
devflow process start --all
devflow process status
devflow process logs web --tail 100

If a hook renders an empty service URL, create the workspace services first (devflow service create <workspace> or devflow switch -c <workspace>) and check the service name used in the template.

.devflow.yml is committed — teammates get the same services, hooks, process definitions, and worktree layout by running devflow init (idempotent; it detects the existing config) or just devflow install-hooks + devflow switch. Hook commands from the config require a one-time approval per user before they execute.