Local Development
The whole platform runs locally from one docker-compose stack:
infrastructure/local/docker-compose.yml. It brings up the three per-solution
Orbit apps, Aurora (the portfolio control plane), and the portfolio registry
(Postgres plus three Hono services) on a private bridge network, bind-mounting
each app’s codebase for hot reload and a reference solution repository for the
agent to author against.
cd infrastructure/local
cp .env.example .env # then edit values (paths, ports, credentials)
docker compose up --buildAll services join the orbit-net bridge network, so containers reach each
other by service name (e.g. http://orbit-agent:3000 from inside another
container). Each service also publishes a host port so the browser can hit it
directly.
Topology
Ports shown are the host-side defaults; every container listens on its own fixed internal port (3000, except the agent-webapp on 3001 and Postgres on 5432).
Services
| Service | Build (context → Dockerfile) | Host port (env var, default) | Container port | Depends on |
|---|---|---|---|---|
orbit-webapp | implementation/ → orbit-webapp/dockerfiles/Dockerfile.local | ORBIT_WEBAPP_PORT, 3000 | 3000 | orbit-agent |
orbit-agent | implementation/ → maxq-orbit-agent/dockerfiles/Dockerfile.local | AGENT_PORT, 3010 | 3000 | — |
orbit-agent-webapp | implementation/maxq-orbit-agent-webapp/codebase → ../dockerfiles/Dockerfile.local | AGENT_WEBAPP_PORT, 3001 | 3001 | orbit-agent |
aurora-webapp | implementation/aurora-webapp/codebase → ../dockerfiles/Dockerfile.local | AURORA_WEBAPP_PORT, 3040 | 3000 | — |
postgres | image postgres:17-alpine | POSTGRES_PORT, 5432 | 5432 | — |
customer-service | implementation/ → customer-service/dockerfiles/Dockerfile.local | CUSTOMER_SERVICE_PORT, 3021 | 3000 | postgres (healthy) |
tenant-service | implementation/ → tenant-service/dockerfiles/Dockerfile.local | TENANT_SERVICE_PORT, 3022 | 3000 | postgres (healthy) |
solution-service | implementation/ → solution-service/dockerfiles/Dockerfile.local | SOLUTION_SERVICE_PORT, 3023 | 3000 | postgres (healthy) |
activity-service | implementation/ → activity-service/dockerfiles/Dockerfile.local | ACTIVITY_SERVICE_PORT, 3024 | 3000 | postgres (healthy) |
Notes on the build contexts:
orbit-webapp,orbit-agent, the three registry services, and the activity service build withimplementation/as the context so theirfile:dependencies — the sharedshared/trajectory-loader,shared/registry-kit, andshared/activity-kitpackages — are inside the build context and get baked into the image.- The registry services and the activity service run their own migrations on
boot, so
upfrom zero yields a working empty registry and activity feed; all four wait for the Postgres healthcheck (pg_isready) before starting.
The volume and mount model
Codebase bind mounts + anonymous volumes
Every Node service follows the same pattern, spelled out in the
Dockerfile.local files:
- Dependencies are installed at image build time (
npm ci/npm install). - At runtime the codebase folder is bind-mounted into the container, so
saved file changes hot-reload via
next dev/tsx watch. - Anonymous volumes are declared on
node_modules(and.nextfor the Next.js apps) — e.g.- /app/.next— so the bind mount doesn’t shadow what the image installed. Without them you’d get macOS-hostnode_modulesinside a Linux container.
For orbit-webapp and orbit-agent the in-container layout mirrors the
repo’s implementation/ folder (/app plays that role): the app lives at
/app/<name>/codebase and the shared @maxq/trajectory-loader package is
baked in at /app/shared/trajectory-loader, so the file:../../shared/...
symlink in node_modules resolves. Changing a shared package therefore
requires an image rebuild — see the workflows below.
The solution mounts (orbit-agent is the sole owner)
The agent is the single writer and sole owner of the solution-repo
mounts. Since the 2026-07-02 refactor, orbit-webapp has no repository
mount at all — it fetches all solution and sources data over HTTP from the
agent’s in-memory model (ORBIT_AGENT_URL: http://orbit-agent:3000, a
docker-network name; the browser never calls the agent directly for this).
| Host path (env var) | Mounted at | Mode | Purpose |
|---|---|---|---|
SOLUTION_REPOSITORY_PATH (required) | /repo | read-write | The customer solution repository; its solution root is workspace/solution-definition/. |
SOLUTION_INTERNAL_REPOSITORY_PATH (defaults to the sibling SOLUTION_REPOSITORY_PATH + -internal) | /repo-internal | read-write | The matching internal repo: agents, catalog, templates, .orbit/ audit trail. |
TRAJECTORY_METHODOLOGY_PATH (defaults to a sibling trajectory-methodology) | /methodology | read-only | The global methodology repo — consulted only by the methodology-upgrade operation; normal authoring resolves the copy vendored inside the customer repo. |
The agent reads these in-container paths from its own environment
(SOLUTION_REPOSITORY_PATH=/repo, etc.); the host paths are chosen purely
via .env.
Aurora’s mounts
Aurora manages the fleet live from GitHub, so it mounts no solution repo. It gets two read-only mounts instead:
- the platform repo root at
/repo-platform:ro— the in-product chat agent’s working directory (AURORA_AGENT_CWD, default/repo-platform), so the agent sees the real workspace root; - a secrets folder at
/secrets:ro—AURORA_SECRETS_PATH(default: the git-ignoredimplementation/aurora-webapp/codebase/secrets/), holding the two GitHub App private-key.pemfiles.
Named volumes (survive down, die on down -v)
| Volume | Attached to | Holds |
|---|---|---|
orbit-claude-sessions | orbit-agent at /root/.claude/projects | Claude Code session transcripts — required to resume: failed/interrupted tasks across container recreates. |
orbit-pg-data | postgres at /var/lib/postgresql/data | The portfolio registry database. |
Environment configuration
Copy .env.example to .env in infrastructure/local/ and fill it in. The
variable names and what they point at (never commit values):
| Variable | What it configures |
|---|---|
ORBIT_WEBAPP_PORT, AGENT_PORT, AGENT_WEBAPP_PORT, AURORA_WEBAPP_PORT, CUSTOMER_SERVICE_PORT, TENANT_SERVICE_PORT, SOLUTION_SERVICE_PORT, ACTIVITY_SERVICE_PORT, POSTGRES_PORT | Host-side published ports (defaults 3000 / 3010 / 3001 / 3040 / 3021 / 3022 / 3023 / 3024 / 5432). |
SOLUTION_REPOSITORY_PATH | Required. Host path of the customer solution repo (must contain workspace/solution-definition/solution.yaml); mounted at /repo in orbit-agent only. |
SOLUTION_INTERNAL_REPOSITORY_PATH | Host path of the internal repo; defaults to the customer repo’s path with -internal appended. |
TRAJECTORY_METHODOLOGY_PATH | Host path of the global methodology repo (read-only mount). |
ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN | The orbit-agent’s Claude credential — set exactly one (API key vs Claude Code subscription token). |
DEFAULT_MODEL, LOG_LEVEL | Agent model and log verbosity. |
GIT_REMOTE_URL, GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_TOKEN | Agent git identity/remote; with GIT_REMOTE_URL empty the agent runs local-only (no clone/push). |
NEXT_PUBLIC_AGENT_URL | Browser-side URL of the agent for Mission Control; defaults to http://localhost: + AGENT_PORT. |
NEXT_PUBLIC_AGENT_WEBAPP_URL | Browser-side link target of the reader’s “Mission Control” rail entry; defaults to http://localhost: + AGENT_WEBAPP_PORT. |
GH_CUSTOMER_ORG / GH_CUSTOMER_APP_ID / GH_CUSTOMER_INSTALLATION_ID and GH_INTERNAL_ORG / GH_INTERNAL_APP_ID / GH_INTERNAL_INSTALLATION_ID | Aurora’s two GitHub Apps (customer solutions org + internal org). Aurora starts fine with these blank — it shows a “Connect Aurora to GitHub” empty state. |
GH_CUSTOMER_PRIVATE_KEY_PATH / GH_INTERNAL_PRIVATE_KEY_PATH (in-container paths under /secrets), GH_CUSTOMER_PRIVATE_KEY / GH_INTERNAL_PRIVATE_KEY (inline alternative), AURORA_SECRETS_PATH | Where Aurora finds the App private keys. |
GH_PROVISION_TOKEN | Cross-org fork credential (classic PAT) for solution creation. |
AURORA_AGENT_ENABLED, AURORA_ANTHROPIC_API_KEY or AURORA_CLAUDE_CODE_OAUTH_TOKEN, AURORA_AGENT_MODEL, AURORA_AGENT_CWD | Aurora’s in-product agent chat (off by default). |
AURORA_TENANTS_ENABLED, POSTGRES_PASSWORD, SERVICE_SHARED_SECRET | Portfolio registry: tenants UI flag, local Postgres password (default orbit-local), and a dormant service-to-service guard (empty = off). |
ORBIT_DEPLOY_ENABLED + AZURE_SUBSCRIPTION_ID, AZURE_RESOURCE_GROUP, AZURE_ACR_NAME, AZURE_ACA_ENV, AZURE_STORAGE_ACCOUNT, AZURE_LOCATION, AZURE_MANAGED_IDENTITY_RESOURCE_ID, AURORA_IMAGE_TAG, ORBIT_WEBAPP_IMAGE_TAG, ORBIT_AGENT_IMAGE_TAG | Optional Azure auto-deploy on solution creation (off by default; non-secret coordinates only — locally your az login session is used). |
Aurora’s Claude credential is deliberately AURORA_-prefixed. Compose
ranks host-shell variables above .env, and host shells often export their
own ANTHROPIC_API_KEY (frequently a subscription sk-ant-oat… token,
which is invalid as an API key). Reading the credential from
AURORA_ANTHROPIC_API_KEY / AURORA_CLAUDE_CODE_OAUTH_TOKEN prevents that
shadowing. The orbit-agent’s ANTHROPIC_API_KEY is not protected this
way — keep your shell clean or set the value in .env explicitly.
Day-to-day workflows
All commands run from infrastructure/local/ (or add
-f infrastructure/local/docker-compose.yml from the repo root). The compose
project is named orbit-local.
# Bring the whole stack up (build images if needed)
docker compose up --build # foreground
docker compose up --build -d # detached
# Bring it down (containers + network; named volumes survive)
docker compose down
# Bring it down AND wipe named volumes (Postgres data, Claude sessions!)
docker compose down -v
# Restart one service (no rebuild, keeps anonymous volumes)
docker compose restart orbit-webapp
# Tail logs
docker compose logs -f orbit-agent # one service
docker compose logs -f --tail=100 # everything
# Status
docker compose ps
# Shell into a container
docker compose exec orbit-agent shRebuilding after a dependency or shared-package change. Because
node_modules lives in the image (the bind mount only carries source), any
change to a package.json/lockfile — and any change to
shared/trajectory-loader or shared/registry-kit, which are baked in at
build time — needs an image rebuild:
docker compose up --build -d orbit-webapp # rebuild + recreate one service
docker compose up --build -d # or the whole stackRenewing anonymous volumes. restart does not recreate anonymous
volumes. To force fresh node_modules (re-seeded from the image) and an
empty .next:
docker compose up -d --force-recreate -V orbit-webappKnown gotchas
globals.css edits don’t always hot-reload
Edits to orbit-webapp’s src/app/globals.css sometimes never trigger a
Turbopack recompile, so the browser keeps serving the previously-compiled CSS
even though the file inside the container is correct. Two things compound:
- macOS → Docker bind-mount file watching is unreliable — inotify events across the virtiofs/gRPC-FUSE bridge are frequently dropped, so Turbopack never sees the change (large-CSS edits drop more often than JS/TSX edits).
.nextis an anonymous volume, anddocker compose restartdoes not recreate it — a plain restart reuses the stale compiled CSS.
The fix — clear .next inside the container, then restart:
docker compose exec -T orbit-webapp sh -c "rm -rf /app/orbit-webapp/codebase/.next/* /app/orbit-webapp/codebase/.next/.* 2>/dev/null"
docker compose restart orbit-webapp
# wait for "Ready in" in the logs, then hard-reload the browser
# (the first request recompiles and is slow)Equivalent: docker compose up -d --force-recreate -V orbit-webapp. A full
down -v + up --build also fixes it, but wipes the Claude session volume.
When verifying a CSS change, don’t trust a single browser reload — confirm
the rule actually shipped (e.g. via getComputedStyle) and clear .next if
it’s stale, rather than assuming the edit was wrong.
Out-of-band solution-repo edits don’t show up in the reader
The agent holds the solution tree in memory and serves it to
orbit-webapp over HTTP; there is no filesystem watcher on /repo.
Edits the agent makes itself are picked up (and pushed to the reader via the
solution.updated SSE event), but changes made out-of-band — editing the
solution repo directly on the host — are invisible until you ask the agent to
reload:
curl -X POST http://localhost:3010/solution/reload(3010 is the default AGENT_PORT.)
Other things to know
down -vdeletesorbit-claude-sessions— interrupted agent tasks can no longer be resumed afterwards. Prefer plaindown, or the targeted--force-recreate -V <service>for volume problems.- The agent container runs as a sandbox trust boundary: compose sets
IS_SANDBOX=1so the Claude Code SDK’sbypassPermissionsmode works under root inside the container. SOLUTION_REPOSITORY_PATHis mandatory — compose fails fast with a clear error (:?Set SOLUTION_REPOSITORY_PATH in .env ...) if it’s unset.