Starflow edge/runner split — runbook
ADR: 047 §1 + §10 Phase 3
Implementation landed: 2026-06-01 (commit a9dd534 follow-up)
Mode today: every prod + dev host runs STARFLOW_ROLE=both (the legacy all-in-one shape). This doc describes the split that’s now possible; the operational rollout is a separate task (see “Rollout” below).
What changed
Section titled “What changed”Starflow now boots in one of three modes, controlled by STARFLOW_ROLE:
| Role | HTTP | Webhooks | Cron | Claim loop | Notes |
|---|---|---|---|---|---|
edge | full (/api/*, /webhooks/*) | yes | yes | off | Enqueues runs; never executes them. |
runner | /health + /readyz only | no | no | on | Claims queued runs from the DB and runs the engine. |
both | full | yes | yes | on | Single-process all-in-one. Default. |
Same binary, same Node code, same starflow.yaml — only env-var-gated subsystems differ.
Why this design
Section titled “Why this design”- One bundle. Two binaries would duplicate the engine + adapter graph. Role-gating costs ~50 lines and keeps the dependency story sane.
- Backward compatible.
STARFLOW_ROLEdefaults toboth, so every existing systemd unit continues to behave identically until an operator opts in. - Same queue, two consumers. The edge and runner share
workflow_runsvia PostgresSKIP LOCKED(today) and will share SpacetimeDB subscriptions (Phase 7). The runner’s claim loop is the only consumer that mutatesclaim_state=queued → claimed; the edge only writesqueued. /readyzis the rolling-deploy contract. When SIGTERM hits, the server flips/readyzto 503 before stopping the claim loop. systemd / Caddy stop sending new requests; in-flight runs keep their leases untildrainTimeoutMsexpires (default 5 min). Anything still running past the deadline is picked up by lease-expiry sweep asrunner_lost— customers see this as a retryable infra error, not a silent data loss.
Env vars
Section titled “Env vars”| Var | Default | Notes |
|---|---|---|
STARFLOW_ROLE | both | edge | runner | both |
STARFLOW_DRAIN_TIMEOUT_MS | 300000 (5m) | Max wait for active runs before forced exit on SIGTERM |
STARFLOW_API_TOKEN | required (edge/both) | Not required for runner (it serves no /api/*) |
STARFLOW_DEFAULT_WORKSPACE | celestial | Used only by webhook/cron triggers (HTTP triggers carry their own per H1) |
STARFLOW_WAIT_TOKEN_SECRET | optional | HMAC key; both processes must share the same secret to verify tokens |
SUPABASE_URL + KEY | both-or-neither | Both processes must point at the same database |
All vars validated at boot in config.ts — misconfigurations fail fast with an aggregated error list.
Rollout plan
Section titled “Rollout plan”This is the next operational step. Code is shipped; deployment is not.
- Verify in dev mirror first. On the dev host:
- Duplicate the existing
starflow.server.api.serviceunit tostarflow.runner.api.service. - Edit each unit’s
EnvironmentFileto setSTARFLOW_ROLE=edgeandSTARFLOW_ROLE=runnerrespectively. - Restart both. Confirm
/healthon each reports its role; confirm a webhook trigger flows edge → DB → runner.
- Duplicate the existing
- Smoke a real run. Trigger
civia the dashboard. Watchjournalctl -u starflow.runner.api.service; the run should appear there, not in the edge logs. - Drain test.
systemctl restart starflow.runner.api.servicewhile a long-running job is in flight; confirm/readyzon the runner returns 503 during drain and the run completes (or hitsdrainTimeoutMsand is reaped by the sweep). - Cron sanity. Confirm scheduled triggers fire exactly once — only the edge unit registers cron jobs. Two processes registering the same cron would double-fire.
- Promote to prod. Same shape on entmoot: split into two units. Caddy continues to proxy
starflow.celestialintelligence.coto the edge unit only. - Validate H1 still holds. Existing pipeline-router workspace-scoping tests must still pass: edge stamps workspace_id on enqueue, runner reads it from the claimed row.
Operational notes
Section titled “Operational notes”- The edge does not run cron. Only one process can own cron — runner role skips registration entirely so split deployments don’t double-fire scheduled triggers.
- Both processes need
SUPABASE_URL. They share the queue. If one points at a stale db, claims silently disappear into the wrong table. STARFLOW_WAIT_TOKEN_SECRETis required on both for durable-wait resumes to work cross-process (edge signs the token at HITL checkpoint, runner verifies it on resume).MESH_WORKSPACEfor heartbeat scope should be the same on both — they report under the same logical “starflow.server.api” service id today. When runner pools materialize, the runner unit will use its own service id (starflow.runner.api).
What’s not in this change
Section titled “What’s not in this change”- No ssmod work. The systemd unit + Caddyfile changes are operations-level and depend on the dev-host validation above. See task #220 (“migrate dev mirror to ADR-043 unit names”) for the unit-naming context.
- No runner pool routing.
runner_poolfilter is already on the claim path (per task #240/#244); wiring per-workspace pool config is ADR-047 Phase 6. - No Fly.io runners. That’s Phase 4.
Reverting
Section titled “Reverting”STARFLOW_ROLE=both (or unsetting it) restores the pre-split shape on either unit. No data-side migration was needed for this phase — it’s pure runtime gating.