Skip to content

Port Allocation (#197)

How BoB assigns dev ports across the fleet. Band convention, the two-tier source-of-truth model, backfill, and inspection. Extracted from the dev lifecycle reference.

← docs home

Every BoB-provisioned project owns a deterministic port band so multiple projects can run their dev servers simultaneously without collisions or unpredictable auto-increment. Port assignment is declarative, version-controlled, reproducible, and owned by exactly one source of truth — per the prime directive.

base = 5200 + slot*10 # contiguous 10-port band per project
base + 0 Vite dev (dev.json server.port)
base + 1 Wrangler dev
base + 2 cds dashboard
base + 3..9 reserved / extensible (other long-running dev services)

slot is derived from a deterministic FNV-1a hash of the project name, so a fresh clone proposes the same band before the ledger has any record of the project. Bands overlapping well-known services (PostgreSQL 5432, VNC 5900) are skipped; extend via the ledger’s reserved_bases.

  1. Authoritative ledger — BOB_SOURCE/provisions/ports.json. The single source of truth, version-controlled (validated by schemas/ports.schema.json). It is the only tier with the global view needed for collision detection. Managed by scripts/port-ledger.js. The ledger lives in BOB_SOURCE — not machine-local BOB_HOME — exactly like provisions/<project>.json.
  2. Derived projection — dev.json + vite/wrangler configs. cdi writes the concrete ports into a marked, regeneratable block (_bob_ports in dev.json; an export const BOB_PORTS = {…} sentinel in vite.config.*; a comment sentinel in wrangler.toml). Never hand-edited — regenerating from the ledger is lossless and idempotent (re-running cdi yields zero diff).
  3. Name-hash = default proposal, not authority. The allocator tries the hash-derived band first; the ledger only records and arbitrates the rare collision (deterministically stepping to the next free band).

Accepted tradeoff: a project’s port lives in the bigbrain repo, not the project’s own repo. Accepted because port allocation is inherently cross-project and BoB already owns per-project state in provisions/.

When cdi runs on a project with an explicit, non-default pinned port (e.g. nanaawards 5180, pinned to coexist with a file:../-linked sibling), that pin seeds the ledger as a preserved pinned entry and is never reshuffled. Framework defaults (5173, 8787, 3000, …) are not treated as pins — otherwise every SvelteKit project would pin to 5173 and perpetually collide.

Terminal window
cdb --ports # port-band table for all projects
cds --ports # same, from the dashboard CLI
node ~/.claude/scripts/port-ledger.js list # raw JSON
node ~/.claude/scripts/port-ledger.js path # ledger location

The legacy machine-local ~/.claude/dashboard-ports.json (the old cds 3333–3400 registry) is folded into the ledger on first use and retired — no parallel registry remains.

cdi only allocates the project it is run on. Projects provisioned before #197 land with no band. scripts/port-fleet-migrate.js is the one-shot migration that backfills the whole fleet at once:

Terminal window
make port-migrate-dry # review the planned project → ports table (zero writes)
make port-migrate # apply: backfill every un-allocated project

Discovery source is BOB_SOURCE/provisions/*.json (minus the ledger, the _default template, and manifests with no resolvable _meta.path). The project name fed to the allocator is basename(_meta.path) — identical to cdi’s PORT_PROJECT_NAME — so the band a project gets here is exactly the band cdi would assign. Properties:

  • Idempotent & stable — a project with an existing ledger band is never reassigned; a second run is a verified no-op (no ledger or file diff).
  • Loud on collision — a pinned band overlapping a reserved range or another project’s band is reported and the run aborts with exit 1 before any write; never silently double-assigned.
  • Confined writes — per-project changes go only through the lossless sentinel-block projector; content outside the marked blocks is preserved.
  • Graceful skips — projects with no vite/Wrangler/dev.json still get a ledger band (reserved) and are listed as ledger-only; missing-on-disk and template manifests are listed as skipped with a reason.

deploy.sh invokes it idempotently and non-fatally on every deploy (honouring the deploy --dry-run flag); the loud exit-1 path is make port-migrate. The end-of-run summary reports assigned / ledger-only / already-allocated / skipped / collisions. Tests: make test-port-fleet-migrate.