Architecture
Overview
Companion Hub is a self-hosting platform built from a few cooperating pieces: a Hub container that runs the API and dashboard, a database and a message queue, a reverse proxy, and one Docker Compose project for each app you install.
Hub is the app runtime layer of the Companion stack. On Companion Core hardware it sits on Companion OS; on any other Linux, Windows, or macOS machine it runs on your own operating system. Everything above it — Companion Memory, agents, and the apps you install — runs on Hub.
Core Components
Hub (Local Server)
The Hub is the main application running on your server:
- Backend: NestJS 12 with TypeScript
- Database: PostgreSQL 14 with Drizzle ORM
- Frontend: React 19 dashboard, served by the backend
- Desktop app: Tauri 2 native wrapper for macOS, Windows, and Linux. It starts and monitors the stack and opens the dashboard.
- Message Queue: RabbitMQ 4 for async task processing
- Reverse Proxy: Traefik v3 for routing and SSL
The next diagram shows what runs where. The Hub container talks to the Docker Engine through the mounted socket, which is how it installs and manages each app. It keeps its state in PostgreSQL and hands slow work, such as installs and updates, to RabbitMQ. Those two services sit on a network that no app can join.
flowchart TB
accTitle: What runs where: the desktop app and dashboard, the ci-hub container, PostgreSQL and RabbitMQ on a private network, Traefik, cloudflared, the optional Tailscale sidecar, app stacks, and host inference engines
subgraph you["Your devices"]
desktop["Tauri 2 desktop app<br/>starts and monitors the stack"]
browser["Browser<br/>React 19 dashboard"]
clients["Agents and editors<br/>MCP and OpenAI-compatible clients"]
end
subgraph machine["Your Hub machine"]
subgraph edge["Edge network: ci-hub_edge"]
traefik["traefik<br/>Traefik v3, ports 80 and 443<br/>(8880 and 8443 in the Linux desktop app)"]
cfd["cloudflared<br/>only when paired"]
ts["hub-tailscale<br/>only with Private VPN"]
end
subgraph gateway["App network: ci-hub_network"]
hub["ci-hub<br/>NestJS 12 API and dashboard, port 5002<br/>/api/mcp and /api/inference"]
apps["Installed apps<br/>one compose project each"]
end
subgraph internal["Private network: ci-hub_internal"]
db[("ci-hub-db<br/>PostgreSQL 14")]
queue["ci-hub-queue<br/>RabbitMQ 4"]
end
docker["Docker Engine<br/>socket mounted read-only"]
engines["Inference engines on the host<br/>Ollama, vLLM, oMLX, Lemonade"]
end
portal["CI Account<br/>hub.ci.computer"]
desktop -->|"docker compose"| docker
desktop -->|"opens 127.0.0.1:5002"| hub
browser --> hub
clients --> hub
hub -->|"reads and writes"| db
hub -->|"queues tasks"| queue
hub -->|"installs and manages"| docker
docker -->|"runs"| apps
traefik -->|"routes by hostname"| hub
traefik -->|"routes by hostname"| apps
cfd --> traefik
ts -->|"tailscale serve"| hub
ts -->|"tailscale serve"| apps
hub -->|"host.docker.internal"| engines
hub -->|"pairing, catalog,<br/>hourly check-in"| portalCI Account (hub.ci.computer)
Your CI Account at hub.ci.computer . Pairing your Hub with it is the first step of setup: until the Hub is paired, the dashboard sends you to its pairing screen instead of the app store. Pairing is also required to install apps from the store, because Hub sends a device key that CI Account issues when you pair. A paired Hub checks in with CI Account about every hour. It provides:
- Device pairing
- Organizations (add people and grant device access)
- Remote Hub management
- Managed Cloudflare Gateway provisioning
- The app catalog and remote App Store access
Cloudflare Gateway
A managed Cloudflare Tunnel that securely exposes your apps without port forwarding. Provisioned and managed through your CI Account.
Docker Layer
All apps run as Docker Compose projects. Each app is isolated in its own compose stack. Every app’s main container joins the shared ci-hub_network, which is how apps reach the Hub API and each other. The Hub’s database and queue are on a separate ci-hub_internal network that apps never see, so an installed app cannot reach them.
A third network, ci-hub_edge, carries public traffic. Only Traefik, cloudflared, and the Tailscale sidecar join it, at fixed addresses, so Traefik and Hub can trust the visitor’s real address from the tunnel and tailnet hops and from nothing else. A fourth network, ci-os-hub_network, is the retired name for the app network. Hub keeps it so apps installed before the rename keep working.
How requests reach an app
Locally, you open an app on its direct port, which bypasses the proxy: http://127.0.0.1:<port> on the Hub machine, or http://<hub-address>:<port> from another device. Hub doesn’t give a locally opened app a hostname, and the app’s page lists each address it has. See Finding your app’s URL. Agent apps and apps with no login of their own publish that port on 127.0.0.1 only, so reach them from another device through Private VPN or the public web. Traefik routes by hostname only for the apps you expose.
Remotely, the Cloudflare Gateway is a managed tunnel that your CI Account provisions when you pair your Hub. The cloudflared container dials out to Cloudflare and holds the connection open, so traffic for an exposed app arrives at Cloudflare, travels down that existing tunnel to your Hub, and reaches Traefik on the edge network — your router never needs an open inbound port. Only apps whose Exposure mode is Public Web get a public URL. Before an app receives a request, Traefik asks the Hub whether the visitor is signed in, for apps you protect with Require Auth.
flowchart TB
accTitle: How requests reach an app: the Cloudflare tunnel and Traefik from anywhere, and a direct port on your LAN
web["Browser anywhere"]
lan["Browser on your LAN"]
account["CI Account<br/>hub.ci.computer"]
cf["Cloudflare network<br/>HTTPS, DDoS protection"]
subgraph hubbox["Your Hub"]
cfd["cloudflared container"]
traefik["Traefik v3"]
auth["ci-hub<br/>forward-auth check"]
app["App container"]
end
web -->|"public URL"| cf
cf -->|"down the outbound tunnel"| cfd
account -.->|"provisions DNS and tunnel"| cfd
cfd --> traefik
lan -.->|"direct port"| app
traefik -.->|"Require Auth apps only"| auth
traefik -->|"routes by hostname"| appAI and agents
Hub does not run the inference engines. Ollama, vLLM, oMLX, and Lemonade run on the host (or on another machine you point Hub at), and Hub reaches them from inside Docker at host.docker.internal. Hub resolves which model and endpoint each AI app should use and writes them into the app’s environment. See AI and inference.
Hub also serves two inference APIs itself. An OpenAI-compatible API at /api/inference/v1 lets an editor or SDK use your hardware, and a pool proxy at /api/inference/pool sends a request to whichever paired Hub has the model and the shortest queue. AI apps use the proxy by default; with no peer paired it serves them from this Hub’s own engines. See Hub Pool.
Agents reach Hub over the Model Context Protocol. The /api/mcp endpoint speaks Streamable HTTP and authenticates each client with a key from Hub’s key store. Agent apps that declare hub_integration.mcp_client receive an endpoint address and a key of their own when you install them. See Companion agent.
Docker Services
The Hub runs several containers defined in the stack’s compose file (docker-compose.prod.yml):
| Service | Image | Purpose | Enabled |
|---|---|---|---|
| ci-hub | Hub backend + frontend | Main application (API on port 5002) | Always |
| ci-hub-db | PostgreSQL 14 | Database. Private network; published on 127.0.0.1:6543 only | Always |
| ci-hub-queue | RabbitMQ 4 | Message broker. Private network; published on 127.0.0.1:5001 only | Always |
| traefik | Traefik v3 | Reverse proxy, SSL, routing (ports 80 and 443, or 8880 and 8443 in the Linux desktop app, and 8080 for the dashboard) | Always |
| hub-tailscale | Tailscale | Private VPN sidecar (compose profile private-vpn) | When you set an auth key or have signed in, and have not opted out |
| cloudflared | Cloudflare tunnel agent | Managed gateway (compose profile cloudflare) | When the Hub is paired |
Hub renamed its containers from the ci-os-hub names to ci-hub. The old names, such as ci-os-hub and ci-os-hub-queue, still resolve as network aliases, so apps and configuration written for them keep working.
Technologies
| Technology | Role | Where it runs |
|---|---|---|
| Docker | Containerisation | Host machine |
| Traefik | Reverse proxy and load balancer | traefik |
| TypeScript | End-to-end type safety | ci-hub |
| NestJS | Backend framework | ci-hub |
| React | Dashboard frontend | ci-hub |
| Tauri 2 | Cross-platform desktop app | Your desktop (macOS, Windows, Linux) |
| Drizzle ORM | Type-safe database access | ci-hub |
| PostgreSQL | Database | ci-hub-db |
| RabbitMQ | Message broker | ci-hub-queue |
| Tailscale | Optional private networking | hub-tailscale |