Four services, one NX monorepo.
┌──────────────────────────────────────────┐
│ Angular PWA (dashboard) │
│ /home (rooms) · /flows · /sessions │
└────────────┬─────────────────▲───────────┘
│ HTTPS REST │ socket.io
▼ │ (live)
┌──────────────────────────────┴───────────┐
│ NestJS orchestrator (main.js) │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ SessionSpawn │ │ Flow Engine v2 │ │
│ │ Service │ │ (BullMQ + cron) │ │
│ └──────┬───────┘ └────────┬─────────┘ │
│ │ ┌────────────────────────┐ │
│ │ │ SSH fleet · MCP tools │ │
│ │ └────────────────────────┘ │
└─────────┼───────────────────┬─────────────┘
│ │
▼ ▼
claude-code PostgreSQL
subprocess (entities, flows,
│ settings, sessions)
│ stdio MCP
▼
┌─────────────────────────┐
│ channel-server (Bun) │ ← spawned per session
│ reply tool · perms │
└─────────────────────────┘
│
├── flow-tools (mcp)
├── cluster-tools (mcp)
├── dashboard-tools (mcp)
├── quick-action-tools (mcp)
└── unifi-tools (mcp) channel-server (Bun)
A per-session MCP relay, spawned alongside every claude-code subprocess. It bridges the
running Claude session and the ChannelDesk hub: exposes a reply tool so the agent can talk
back to whichever channel triggered it, and a permission relay that surfaces tool-permission prompts
in the UI rather than stdin. Built on Bun + the MCP SDK.
orchestrator (NestJS)
Houses the brains. It's responsible for:
- SessionSpawnerService — spawns
claude-codesubprocesses, wires per-session MCPs, streams stdout. - Flow Engine v2 — BullMQ-backed async pipeline; cron, webhook and event triggers via the FlowEventBus. Uses
@nestjs/schedule. - SettingsService — persisted user + global settings, model allow-list validation.
- FleetModule — SSH host registry, key store, session manager. Sync + command adapters per integration (UniFi, Hue, custom).
- WebSocket gateway — Socket.IO, streams session output and device state to the UI.
MCP tool servers
Out-of-process MCP servers, one binary each, all under packages/. Spawned by the
orchestrator and wired into Claude sessions on demand:
flow-tools— read/run/inspect flows from inside a sessioncluster-tools— query and act on the running k8s clusterdashboard-tools— push updates and notifications to the UIquick-action-tools— pre-defined action shortcuts the agent can invokeunifi-tools— UniFi network operations
frontend (Angular PWA)
Installable, offline-capable control desk. APP_INITIALIZER blocks bootstrap until settings are
loaded — no more hardcoded fallback labels. Service worker registered in app.config.ts.
The /home route is room-centric; every control uses optimistic updates.
registry (PostgreSQL)
Every entity — devices, rooms, flows, sessions, settings — lives in Postgres. The registry is the source of truth; sync adapters reconcile it with the real world. Crash-safe by design.
How a message flows
- A trigger fires — chat message in the dashboard, cron tick, SSH event, or webhook.
- The orchestrator picks the matching flow and calls
SessionSpawnerService.spawnForFlow(), which forks aclaude-codesubprocess. - A
channel-serverBun MCP is spawned alongside the session, exposing thereplytool and a permission relay back to the hub. - Additional tool MCPs (
flow-tools,cluster-tools,dashboard-tools,quick-action-tools,unifi-tools) are wired in based on the flow's manifest. - Claude streams tokens → Socket.IO → any subscribed dashboard client. Device actions go through the capability layer; state lands in Postgres; the UI updates optimistically.
- The session's final reply is delivered through the channel-server's
replytool back to the inbound channel.
See the remote control page for how resumable sessions and offline-first UI cooperate when your network drops out.
In pixels
The diagram above as it actually renders on a running install — one shot of the orchestrator looking at itself, one shot of a live session streaming back through the channel-server.