Skip to content

Port Allocation

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 docs-site dev (scripts/docs-site/dev.sh)
base + 4..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 are not treated as pins — otherwise every SvelteKit project would pin to 5173 and perpetually collide. The recognised defaults are the FRAMEWORK_DEFAULT_PORTS set in scripts/port-project-sync.js: Vite 5173/5174, Wrangler 8787/8788, Next/Express 3000/3001, Astro 4321, Angular 4200, Hugo 1313, Django 8000. A default missing from that set is a collision waiting to happen — the first project of that framework is recorded as pinned to it and the second fails make port-migrate (#1919, Astro).

Astro needs the port on the command line. Unlike Vite and Wrangler, the sync writes no Astro config block, so an assigned band only takes effect when dev.json server.command passes it — astro dev --port <base>. Check the command after a band is assigned or migrated.

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.