Get OpenFlows running on a fresh machine in 10 steps. For what OpenFlows is and how it works, see the README.
Working directory: all commands run from the project root (the directory containing
docker-compose.yml). No need tocdinto subdirectories.
- Prerequisites
- Step 1 — Create a GitHub App
- Step 2 — Set up .env
- Step 3 — Start Docker
- Step 4 — Sign in with GitHub
- Step 5 — Get your Coder session token
- Step 6 — Configure an LLM model
- Step 7 — Test the AI setup
- Step 8 — Bootstrap
- Step 9 — Add a tenant
- Step 10 — Run the controller
- Verify it's working
- Lifecycle hooks
- Configuration
- Troubleshooting
- More
- Docker 24+
- Rust 1.70+ (builds the
openflowsbinary during bootstrap) - The
coderCLI on yourPATH— bootstrap shells out tocoder templates push:curl -fsSL https://coder.com/install.sh | sh - A GitHub account.
OpenFlows agents authenticate to your GitHub repos (including private ones) through a GitHub App. Create a new one at https://github.com/settings/apps/new:
Important: If your repository belongs to an organization, you must create the GitHub App directly within that organization's settings (
https://github.com/organizations/<your-org>/settings/apps), not your personal account.
- Set the Redirect URI (under Identifying and authorizing users) to exactly:
http://localhost:7080/external-auth/primary-github/callback - Under Permissions → Repository permissions, set Contents, Pull requests, and Workflows to Read and write.
- Create the app, then Install it on your org via your install URL (
https://github.com/apps/<your-app-slug>/installations/new). - Copy three values for Step 2: the Client ID, a generated Client Secret (shown once), and your Install URL.
If you change the app's permissions after installing, re-open the installation at https://github.com/settings/installations and click Approve/Update, then get a fresh token — otherwise the new permissions don't take effect.
Create your .env from the template:
cp .env.example .envFill in the required values:
| Variable | What to put |
|---|---|
CODER_CHAT_HOOK_SECRET |
The shared signing secret for lifecycle hooks. Generate 32+ random bytes: openssl rand -hex 32. The bundled stack requires it before enabling hooks (see Lifecycle hooks). |
CODER_SESSION_TOKEN |
Leave empty for now — you'll fill it in Step 5. |
Note: The target repo is not configured in
.env. It is bound per-tenant in Step 9 via./scripts/prod.sh tenant <owner/repo> --name <team> --fleet <N>. Each tenant gets its own nexus workspace and controller scoped to that repo.
Then set the three GitHub external auth values in .env from Step 1:
CODER_EXTERNAL_AUTH_0_ID=primary-github
CODER_EXTERNAL_AUTH_0_TYPE=github
CODER_EXTERNAL_AUTH_0_CLIENT_ID=<your-github-app-client-id>
CODER_EXTERNAL_AUTH_0_CLIENT_SECRET=<your-github-app-client-secret>
CODER_EXTERNAL_AUTH_0_SCOPES=repo
CODER_EXTERNAL_AUTH_0_APP_INSTALL_URL=https://github.com/apps/<your-app-slug>/installations/newSet these before starting Docker — Coder reads them from
.envat startup, and won't start with empty GitHub App credentials.
First, make sure ports 6379 (Redis) and 7080 (Coder) are free so there's no conflict. If another, unrelated container is already holding one of those ports, find and remove only that one by name:
docker ps --filter "publish=6379" --filter "publish=7080"
docker rm -f <conflicting-container-name>Then bring up Redis, the Coder database, and the Coder server:
docker compose up -dWait until all three report healthy:
docker compose psNext: visit your app's Install URL and install the GitHub App on your org/repos so the app can access them.
Open http://localhost:7080 and sign in with your GitHub account (Coder's device flow).
To confirm the GitHub App was set up correctly, open http://localhost:7080/deployment/external-auth — you should see a row with ID primary-github (with its Client ID and Match). If the row is missing, the external auth vars weren't picked up; see the troubleshooting note below.
Signing in is not enough. Each tenant workspace is owned by its tenant user (created by tenant add), and Coder refuses to build a workspace until the owning account links the GitHub App. The CODER_SESSION_TOKEN account is only the privileged provisioning actor — the tenant user is the actual workspace owner.
Since tenant add performs a real API-driven grant check (it polls GET /api/v2/external-auth/primary-github until the tenant user's GitHub link is confirmed), you no longer need to pre-link the app manually. When you run tenant add, it prints the link URL and (when a device flow is available) a one-time code you can authorize in a browser — complete that and onboarding continues automatically:
- Run
./scripts/prod.sh tenant <owner/repo> --name <my-team> --fleet <N>. - Onboarding prints a "GitHub Link Required" prompt with either:
- a device-flow URL + one-time code (fastest — open it, enter the code, click Authorize), or
- the Coder dashboard link
http://localhost:7080/external-auth/primary-githubto authorize as the tenant user.
- Once you authorize,
tenant addconfirms the linked GitHub account (user.login) and proceeds to create the tenant workspace automatically — no need to press Enter.
Do not link the GitHub App only as the
CODER_SESSION_TOKEN(admin/provisioning) account — that account provisions the workspace but does not own it, so tenant workspace creation would still be blocked by Coder's external-auth403.
Required for private repos: skipping this link makes bootstrap/
tenant addtime out (or fail with403 External authentication is required to create a workspace with this template); see Troubleshooting.
- Open http://localhost:7080/settings/tokens
- Click Create Token, copy it.
- Paste it into
.env:CODER_SESSION_TOKEN=your_token_here
OpenFlows agents need at least one model.
- Go to http://localhost:7080/ai/settings/providers
- Add a provider (e.g. OpenAI, Anthropic).
- Go to http://localhost:7080/ai/settings/models and add a model to that provider (e.g.
deepseek-v4-flash-0731).
Open http://localhost:7080/agents and confirm agents/models show up. Say "hello" in the chat to verify the model responds.
Run the one-time setup to initialize Coder with the OpenFlows templates and config:
./scripts/prod.sh bootstrapThis builds the openflows binary into .dev-binaries/, creates the admin user, pushes the workspace templates, and verifies GitHub/LLM auth.
Confirm the templates were pushed at http://localhost:7080/templates.
Bind a GitHub repo to OpenFlows. A tenant is scoped to a single owner/repo and provisions its own nexus workspace + controller:
./scripts/prod.sh tenant <owner/repo> --name <my-team> --fleet 3--fleet N sets the number of FORGE-SENTINEL pairs for the tenant: N forge worker slots and N sentinel worker slots (a fleet of 3 → forge-1..forge-3 and sentinel-1..sentinel-3). It is mandatory and must be >= 1. The fleet value is written into the tenant's registry.json (also persisted to the tenant store), which the in-workspace controller reads and applies automatically.
tenant add now onboards self-serve:
- Creates the tenant-owner Coder user.
- Checks (via the Coder API, as that user) whether GitHub is linked, and — if not — prints a device-flow one-time code and link URL to authorize.
- Once GitHub is linked, mints a tenant token and provisions the
openflows-nexus-<tenant>workspace, which auto-starts its controller.
You'll see the tenant's nexus workspace under http://localhost:7080/workspaces.
GitHub auth: the GitHub App (Step 1) is the sole source of the GitHub token — each tenant links their GitHub account during
tenant add, and the controller/agents use that linked token. No PAT is required.
Note: Each tenant is isolated (per-tenant Redis namespaces, separate workspaces/controllers). The model supports multiple tenants; running several concurrently is part of the design and still being validated — start with one tenant per controller host for now.
Upgrading from an earlier setup? Tenant workspaces created before this change were built with
start_controller=falseand are returned unchanged if you re-runtenant add. Recreate an existing tenant's workspace once to pick upstart_controller=true(the controller then auto-starts inside it). The same applies to--fleet:tenant addon an existing nexus workspace leaves its build parameters (including the fleet) unchanged, so the fleet value only applies to newly added tenants, or after you recreate the tenant's workspace. New tenants get these automatically — nothing extra to do.
The controller runs inside the tenant's nexus workspace and auto-starts when the workspace is ready. You don't run it on your machine — the workspace was created with start_controller enabled, and its GITHUB_REPOSITORY/OPENFLOWS_TENANT are injected from the tenant you added.
Note: There is no host-side
runcommand — the controller runs inside the tenant's nexus workspace and auto-starts when the workspace is ready (started bytenant add). Running the controller manually on the host has been removed as a duplicate of that in-workspace auto-start.
Create a GitHub issue in the bound repo → OpenFlows automatically assigns it, provisions a workspace, and starts working.
In a separate terminal:
./scripts/prod.sh doctorLifecycle hooks let OpenFlows observe and steer agent behaviour as it happens. The bundled stack wires them automatically — you only provide the shared signing secret in Step 2. Do not set the Coder hook experiment/URL/bind variables manually.
After Step 10, confirm the consumer started and the stack is not logging InvalidAudience. For explicit confirmation, set OPENFLOWS_HOOK_LOGS=true in .env, restart the controller, and watch for:
INFO ... Coder lifecycle hook consumer started
You can also exercise the consumer directly with the openflows binary's simulate command (run it from a shell that can reach the consumer, using the same secret):
export CODER_CHAT_HOOK_SECRET=<the same secret you put in .env>
export CODER_CHAT_HOOK_URL=http://openflows-nexus:3001/experimental/hooks/chat
./.dev-binaries/openflows hooks simulate --event user_prompt_submit --chat-id chat-verifyA healthy consumer responds 200. If you see InvalidAudience, see Troubleshooting.
CODER_CHAT_HOOK_SECRET is an immutable tenant workspace parameter: Coder captures it when the workspace is built, and re-running bootstrap alone does not update an existing workspace (bootstrap only pushes templates; it no longer creates a workspace). To rotate, you must recreate the tenant workspace so it is rebuilt with the new secret:
- Stop the tenant's Controller.
- Set a new 32+ byte
CODER_CHAT_HOOK_SECRETin.env. - Delete the tenant's existing Nexus workspace so it can be rebuilt. From the host with the
coderCLI, logged in as the tenant user:(If you changedcoder delete openflows-nexus-<tenant>
OPENFLOWS_HOOK_URLat the same time, do the same — the hook URL is also immutable.) - Recreate the workspace through the tenant flow, which rebuilds it with the new secret and restarts its controller:
./scripts/prod.sh tenant <owner/repo> --name <tenant>
Coder and the consumer must always share the same secret, and both Coder and the Controller must be restarted after rotation.
These are optional — the defaults work out of the box. Only touch them if you need to.
| Variable | Default | Notes |
|---|---|---|
CODER_ADMIN_USERNAME |
admin |
Admin account created by bootstrap. |
CODER_ADMIN_EMAIL |
admin@openflows.dev |
|
CODER_ADMIN_PASSWORD |
Op3nFl0ws! |
Must be ≥8 chars with upper, lower, digit, and special char — otherwise bootstrap silently falls back to the default. |
REDIS_URL |
redis://localhost:6379 |
Set only if you host Redis elsewhere. |
CODER_URL |
http://localhost:7080 |
Set only if you host Coder elsewhere. |
OPENFLOWS_TENANT |
default |
Namespace for Redis keys. |
CODER_CHAT_HOOK_SECRET |
required | Generate 32+ random bytes, for example openssl rand -hex 32; hook URL/experiment/bind values are wired automatically. |
OPENFLOWS_HOOK_URL |
http://openflows-nexus:3001/experimental/hooks/chat |
Single source of truth for the hook endpoint, shared between Coder and the consumer. The consumer picks it up via bootstrap; after changing it on an existing deployment, recreate the Nexus workspace (see Rotating the secret). |
OPENFLOWS_HOOK_LOGS |
false |
Set true only when debugging lifecycle hook traffic. |
SLACK_WEBHOOK_URL / DISCORD_WEBHOOK_URL |
unset | Escalation notifications. |
When a team member signs in with GitHub OAuth, Coder creates them as a regular member, who can't create workspaces or push templates. If you want OpenFlows to run as that user, grant them these roles (or bootstrap fails with 403 Unauthorized to create workspace):
| Role | Why |
|---|---|
organization-admin |
Provision workspaces (e.g. tenant nexus workspaces) + template management. |
organization-template-admin |
Push/update the openflows-* templates. |
organization-workspace-access |
Required for org workspaces. Keep it — edit-roles replaces the whole role set. |
Trap:
organization-workspace-creation-bancarries a negativeworkspace:createpermission that overridesorganization-admin. If you see403 Unauthorized to create workspace, make sure this role is not assigned.
Via CLI:
export CODER_URL=http://localhost:7080
export CODER_SESSION_TOKEN=<your-token>
# List orgs, then grant roles (include ALL existing roles or they'll be removed)
coder organizations list
coder organizations members edit-roles -O=<org> <username> \
organization-admin \
organization-template-admin \
organization-workspace-accessOr via the dashboard: Admin settings → Organizations → <your org> → Members → Edit roles and select the roles above.
coder is missing or not on your PATH. Install it and re-run bootstrap:
curl -fsSL https://coder.com/install.sh | sh
coder versionOpen http://localhost:7080/ai/settings/providers and add a provider/model, then re-run bootstrap.
The .dev-binaries/ directory is root-owned:
sudo chown -R "$USER":"$USER" .dev-binaries/Another process/container holds port 6379. Stop or remove the conflicting container, or change the Redis port mapping in docker-compose.yml.
Confirm the external auth vars are set in .env before running docker compose up -d, then restart Coder:
docker compose restart coderThen verify the provider at http://localhost:7080/external-auth (or the admin external-auth page).
Grant the GitHub App Workflows → Read and write (this is a different permission from Actions), then approve/update the installation and get a fresh token — see the note in Step 1.
Grant the GitHub App Pull requests → Read and write, then approve/update the installation and get a fresh token — see the note in Step 1.
- Confirm a tenant is bound (
./scripts/prod.sh tenant <owner/repo> --name <my-team>). - Watch the controller's foreground terminal for errors.
- Verify Coder is reachable:
curl http://localhost:7080/api/v2/buildinfo.
This usually means the hook consumer rejected or couldn't be reached during Coder Chat creation. The most common specific cause is the one below (InvalidAudience). Other possibilities: the consumer isn't running (no CODER_CHAT_HOOK_SECRET set), or the Coder container can't reach the consumer endpoint. Confirm the secret is set in .env (Step 2), the controller is running, and — for a custom topology — that the hook URL is reachable from the Coder container.
If the controller logs Hook consumer: JWT verification failed ... InvalidAudience, the JWT's aud claim (Coder's CODER_CHAT_HOOK_URL) does not match the audience the consumer expects. In the bundled stack this should not happen — OPENFLOWS_HOOK_URL drives both Coder's URL and the consumer's expected aud via bootstrap. It appears when those two get out of sync:
- The hook URL is set on a custom deployment via
OPENFLOWS_HOOK_URL, but the tenant workspace was not recreated after the URL changed. The hook URL is an immutable workspace parameter, so an existing workspace keeps validating against the audience it was originally built with even after you changeOPENFLOWS_HOOK_URL. Recreate the tenant workspace (see Rotating the secret) so it is rebuilt against the current URL, or keep the bundled defaults. - You manually set
CODER_CHAT_HOOK_URLorOPENFLOWS_HOOK_ADDRin your.envwhile using the default Docker setup. These variables are for custom host deployments only; the standard Docker setup automatically configures the internal networking. Remove them from.envand recreate the workspace. - Coder and the consumer were started with different
CODER_CHAT_HOOK_SECRETvalues or one was restarted out of order. Restart both with the same secret.
Coder refuses to build a workspace until the owning account links the GitHub App (workspaces that request GitHub access require the owner to authenticate with it). Fix it by completing the link as the tenant user (see Link the GitHub App) — sign in as the workspace owner and visit http://localhost:7080/external-auth/primary-github, then Authorize on GitHub. Afterwards, recreate the tenant workspace via ./scripts/prod.sh tenant <owner/repo> --name <tenant>.
Even with the provider configured, git access only works when all three are true:
- The workspace owner has linked the GitHub App (see Step 4).
- The GitHub App is installed on the account/org that owns the repo, with access to it (
https://github.com/apps/<your-app-slug>/installations/new). - The App is set to public (App → Advanced → "Make this GitHub App public") so other accounts can link it.
Verify inside a workspace without printing the token: test -s ~/.git-credentials && echo 'git credentials configured' (or run git ls-remote <your-repo-url> to confirm auth works). Never cat ~/.git-credentials — it prints your live GitHub token to the terminal/session logs.
- Full docs: README.md
- Testing & debugging: testing_quick_start.md
- Token acquisition: token_guide.md
- Lifecycle hooks (design): docs/experiments/hook-driven-state-derivation.md and docs/experiments/coder-lifecycle-hooks-feedback.md