Skip to content

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).

Starflow now boots in one of three modes, controlled by STARFLOW_ROLE:

RoleHTTPWebhooksCronClaim loopNotes
edgefull (/api/*, /webhooks/*)yesyesoffEnqueues runs; never executes them.
runner/health + /readyz onlynonoonClaims queued runs from the DB and runs the engine.
bothfullyesyesonSingle-process all-in-one. Default.

Same binary, same Node code, same starflow.yaml — only env-var-gated subsystems differ.

  • One bundle. Two binaries would duplicate the engine + adapter graph. Role-gating costs ~50 lines and keeps the dependency story sane.
  • Backward compatible. STARFLOW_ROLE defaults to both, so every existing systemd unit continues to behave identically until an operator opts in.
  • Same queue, two consumers. The edge and runner share workflow_runs via Postgres SKIP LOCKED (today) and will share SpacetimeDB subscriptions (Phase 7). The runner’s claim loop is the only consumer that mutates claim_state=queued → claimed; the edge only writes queued.
  • /readyz is the rolling-deploy contract. When SIGTERM hits, the server flips /readyz to 503 before stopping the claim loop. systemd / Caddy stop sending new requests; in-flight runs keep their leases until drainTimeoutMs expires (default 5 min). Anything still running past the deadline is picked up by lease-expiry sweep as runner_lost — customers see this as a retryable infra error, not a silent data loss.
VarDefaultNotes
STARFLOW_ROLEbothedge | runner | both
STARFLOW_DRAIN_TIMEOUT_MS300000 (5m)Max wait for active runs before forced exit on SIGTERM
STARFLOW_API_TOKENrequired (edge/both)Not required for runner (it serves no /api/*)
STARFLOW_DEFAULT_WORKSPACEcelestialUsed only by webhook/cron triggers (HTTP triggers carry their own per H1)
STARFLOW_WAIT_TOKEN_SECREToptionalHMAC key; both processes must share the same secret to verify tokens
SUPABASE_URL + KEYboth-or-neitherBoth processes must point at the same database

All vars validated at boot in config.ts — misconfigurations fail fast with an aggregated error list.

This is the next operational step. Code is shipped; deployment is not.

  1. Verify in dev mirror first. On the dev host:
    • Duplicate the existing starflow.server.api.service unit to starflow.runner.api.service.
    • Edit each unit’s EnvironmentFile to set STARFLOW_ROLE=edge and STARFLOW_ROLE=runner respectively.
    • Restart both. Confirm /health on each reports its role; confirm a webhook trigger flows edge → DB → runner.
  2. Smoke a real run. Trigger ci via the dashboard. Watch journalctl -u starflow.runner.api.service; the run should appear there, not in the edge logs.
  3. Drain test. systemctl restart starflow.runner.api.service while a long-running job is in flight; confirm /readyz on the runner returns 503 during drain and the run completes (or hits drainTimeoutMs and is reaped by the sweep).
  4. Cron sanity. Confirm scheduled triggers fire exactly once — only the edge unit registers cron jobs. Two processes registering the same cron would double-fire.
  5. Promote to prod. Same shape on entmoot: split into two units. Caddy continues to proxy starflow.celestialintelligence.co to the edge unit only.
  6. 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.
  • 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_SECRET is required on both for durable-wait resumes to work cross-process (edge signs the token at HITL checkpoint, runner verifies it on resume).
  • MESH_WORKSPACE for 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).
  • 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_pool filter 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.

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.