ADR-0004: Web Hub is a singleton FastAPI serving every registered project
- Status: Accepted (2026-05-01)
- Deciders: Kourosh Ebrahimzadeh
- Context tags: web, multi-project, ports, devx
Context
Each coding-os project has rich visualizable state: a knowledge graph, a Scrumban board, a cognition trace, search across four retrieval layers. The original design was per-project port — every project booted its own FastAPI on a chosen port and you opened the right URL.
Two problems emerged:
- Port management. A user with five projects had five ports to remember + manage in browser tabs. Conflicts when two projects defaulted to the same port. Tooling to allocate ports added complexity.
- Cross-project context. Looking at project A's graph and project B's board required two tabs. There was no obvious "switcher" — the UI assumed a single project.
Decision
Singleton FastAPI on a fixed port (9188) that serves every registered project via path-prefixed routes:
/ → project picker / dashboard
/api/p/<slug>/graph → project <slug>'s graph routes
/api/p/<slug>/board → project <slug>'s board routes
/api/p/<slug>/cognition → project <slug>'s cognition trace
/api/p/<slug>/search → unified search across <slug>'s layers
/api/stream/events → SSE event stream (multiplexed across projects)
Projects register themselves on cos init (writes a line to
~/.coding-os/registry.json). The hub discovers them at boot;
adding a new project is a matter of cos hub status --refresh
without a restart.
cos hub start boots the hub. cos hub status reports running
state + the meta-repo path + symlink health for every registered
project.
Consequences
Positive:
- One URL to remember (
http://127.0.0.1:9188). - One process to manage (start/stop/restart).
- Side-by-side visibility across projects — the dashboard lists every registered project as a tile.
- Easier to share with a human collaborator on the same machine (port-forward one port instead of N).
- SSE event stream can fan out updates from any project to any subscriber.
Negative:
- One process means one crash takes down all projects' UIs. Mitigated by the lightweight FastAPI surface — no long-lived state, restart is sub-second.
- Path-prefix routes are slightly more verbose to write in
frontend code (every fetch needs the slug). Mitigated by the
useProject()React hook that hides the prefix. - The fixed port (9188) can conflict with other tools. Mitigated
by
COS_HUB_PORTenv var override + clear error message on EADDRINUSE.
Mitigations / follow-ups:
- The frontend SPA is built once and cached; the FastAPI just serves the static bundle + the per-project APIs.
- A future "embed mode" where the hub mounts inside a larger IDE-style shell is straightforward — it's already a separated FastAPI + React boundary.
Alternatives considered
- Per-project ports. Original design. Rejected — see Context.
- One FastAPI per project, fronted by a reverse proxy. Adds operational complexity (configure nginx/Caddy) for no meaningful gain over the singleton.
- No web UI; CLI-only. Rejected — the graph and board are hard to read in a terminal at any nontrivial scale.