Run a headless server
Run cc_server on a machine that stays on, then connect to it from your desktop,
a browser, or your phone. The server is a pure-Dart native binary — no Flutter
engine — so it runs headless on macOS, Linux, or Windows. It owns the database,
runs the agents and makes every external API call; the clients only render.
Run in Docker (prebuilt images)
Section titled “Run in Docker (prebuilt images)”Every release publishes a self-hosted stack of four GHCR images — the backend,
the two static thin clients and the optional pairing relay — plus a separate
cc-server-demo image for the public demo host. A self-hosted install needs
no source checkout. Pin a version tag (ghcr.io/samuelalev/<image>:v0.0.1-rc.1)
or ride latest.
| Image | What it is | Publishes |
|---|---|---|
ghcr.io/samuelalev/cc-server |
The backend: database, agents, MCP, webhooks | -p 9030:9030 |
ghcr.io/samuelalev/cc-webapp |
The web client (static nginx) | -p 8080:8080 |
ghcr.io/samuelalev/cc-remote |
The phone companion PWA (static nginx) | -p 8081:8080 |
ghcr.io/samuelalev/cc-signaling-server |
The WebRTC pairing relay (optional) | -p 8788:8788 |
The whole stack as a compose file:
services: server: image: ghcr.io/samuelalev/cc-server:latest ports: ["9030:9030"] volumes: ["cc_data:/data"] restart: unless-stopped web: image: ghcr.io/samuelalev/cc-webapp:latest ports: ["8080:8080"] restart: unless-stopped remote: image: ghcr.io/samuelalev/cc-remote:latest ports: ["8081:8080"] restart: unless-stopped signaling: image: ghcr.io/samuelalev/cc-signaling-server:latest ports: ["8788:8788"] restart: unless-stopped
volumes: cc_data:Things worth knowing before you expose any of this beyond loopback:
-
The
cc-serverimage shipsCC_SERVER_INSECURE=1(it must bind0.0.0.0to receive published-port traffic), which is only safe behind a TLS-terminating reverse proxy — Caddy, Traefik, nginx or a Cloudflare Tunnel — on a trusted network. To terminate TLS in-process instead, mount a certificate and setCC_SERVER_TLS_CERT/CC_SERVER_TLS_KEY(then unsetCC_SERVER_INSECURE). Everything is overridable withdocker run -e …or the composeenvironment:block. -
/datais the only state. The SQLite databases, paired-device secrets, downloaded models and meeting audio all live in the volume — back it up and the container becomes disposable. -
Pair a client from inside the image before the first connection, the container equivalent of step 3:
Terminal window docker run --rm -it -v cc_data:/data \--entrypoint /app/bin/cc_server ghcr.io/samuelalev/cc-server:latest pair -
The two client images are static nginx hosts — nothing to configure at runtime; each client’s connect form takes your server URL, device id and pairing key. The signaling relay likewise needs only its port (
SIGNALING_HOST/SIGNALING_PORToverridable); it is stateless, holds no secrets and is a fallback — a phone uses loopback, LAN, tailnet or a directwss://path when one is reachable, and only falls back to the relay when none is. Skip it if your clients always reach the server directly.
The rest of this guide builds the server from source; the flags and behavior it covers are the same ones the image’s environment variables feed.
Prerequisites
Section titled “Prerequisites”- The Dart SDK. This repo pins its SDK with fvm, so prefix every command with
fvm. - This repository checked out.
- The native toolchain the staging script needs — Rust/
cargo, plusmeson,ninja,pkg-config,gitand a C++ compiler. Each build script names what it cannot find and how to install it. - A reachable host: loopback for local use, or a LAN/public address with TLS for remote clients.
Step 1: Stage the native libraries
Section titled “Step 1: Stage the native libraries”The natives are required — there is no degraded mode. Build them from the repo root before anything else:
scripts/natives/build_natives.shThis builds rift, fff, tree-sitter plus its grammars, aec, lame,
ccpty, cc_watcher, cc_inference and the SAML native into
build/natives/. The cc_server build
hook re-emits whatever is staged there into the bundle, so skipping this step
either fails the build outright or — with the escape hatch below — produces a
binary whose boot preflight refuses to start and names the missing library.
Step 2: Build the binary
Section titled “Step 2: Build the binary”cd apps/cc_serverfvm dart build cliThe bundle ships libsqlite3 and the staged natives alongside the binary — no
system SQLite and no Flutter engine needed. The executable lands at
./build/cli/<os_arch>/bundle/bin/cc_server.
Step 3: Pair a client before first start
Section titled “Step 3: Pair a client before first start”A fresh data directory has no paired device and no pre-shared key, so a client’s
pairing-key prompt cannot be satisfied yet. Mint one with the pair subcommand.
It writes global.db and secrets.json directly; a server that is already
serving that data dir picks the new device up without a restart:
./build/cli/<os_arch>/bundle/bin/cc_server pair --data-dir ./data --port 9030It prints three values to paste into a client’s connect form:
Connect a thin client (paste into its connect form): Server ws://localhost:9030/rpc Device id web-client Pairing key <a freshly generated key>Useful flags: --device (default web-client; the id also picks the device
tier — desktop, ios, android, anything else is a web client), --label
(defaults to the platform name plus this machine’s hostname), --host (the host
to print in the URL, since loopback is not reachable from another machine) and
--client-url. Re-running pair rotates that device’s key and drops its live
sessions. Pairing never creates a workspace: the connecting client’s onboarding
names and creates the first one.
With --client-url it also prints a terminal-scannable QR that opens the web
client already filled in:
./build/cli/<os_arch>/bundle/bin/cc_server pair --data-dir ./data --port 9030 \ --bind any --host 192.168.1.42 --client-url https://app.usectrl.devA credential minted by cc_server pair carries no expiry; one minted in the
app always expires after 30 days. Revoke a CLI-minted device from
Settings → You → Your devices when you are done with it.
Step 4: Start the server
Section titled “Step 4: Start the server”On loopback — the default and all you need for a client on the same machine:
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030To reach it from another machine, bind every interface. A non-loopback bind
without TLS is refused at startup (Refusing to bind non-loopback address … without TLS), so supply a certificate:
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030 \ --bind any --tls-cert /path/fullchain.pem --tls-key /path/privkey.pem \ --public-url wss://server.example:9030/rpcOr terminate TLS in a reverse proxy on a trusted private network and opt into a plaintext bind behind it:
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030 \ --bind any --insecure --public-url wss://server.example/rpc--insecure is ignored when --tls-cert and --tls-key are both set — TLS
always wins. The server advertises the plaintext state as insecure: true in
GET /healthz and clients badge such a connection as insecure.
The server runs until SIGINT/SIGTERM, then shuts down cleanly.
Step 5: Connect a client
Section titled “Step 5: Connect a client”Point a client at the server URL and pairing key from step 3:
- Desktop app: on the first-run screen (How should Control Center run?) fill in the connect fields instead of Run in this app, or add the server later at Settings → Server → Connection & status. See Connect to a remote server.
- Web client: open app.usectrl.dev (or your own copy of the web build) and enter the server URL, device id and pairing key.
- Phone: pair the Remote app from a running client — see Pair a device.
Step 6: Connect a Google Calendar (optional)
Section titled “Step 6: Connect a Google Calendar (optional)”To sync a workspace’s calendar with no GUI attached, use the device-code flow:
GOOGLE_OAUTH_CLIENT_ID=<id> GOOGLE_OAUTH_CLIENT_SECRET=<secret> \ ./build/cli/<os_arch>/bundle/bin/cc_server calendar connect \ --data-dir ./data --workspace <workspace-id>It prints a code and a URL to approve on another device, stores the refresh token
server-side and exits. A source build ships no Google client, so the id and
secret environment variables are required — there is no --google-client-id
flag. See Connect a Google Calendar.
Configuration
Section titled “Configuration”The flags used above are the ones a first setup needs. The complete surface — 23
flag and environment-variable pairs, the four subcommands and the data-directory
layout — is in the cc_server CLI reference.
The four you are most likely to need:
| Flag | Default | Meaning |
|---|---|---|
--data-dir |
the OS per-user application-data dir | Databases, secrets, models and cached media |
--port |
9030 |
TCP port (0 = ephemeral) |
--bind |
loopback |
loopback, or any for every interface (needs TLS or --insecure) |
--public-url |
derived from the bind | The RPC URL advertised to paired clients. Set this explicitly behind a proxy, NAT, or tunnel |
The default data dir is ~/Library/Application Support/control-center on macOS,
%APPDATA%\control-center on Windows and $XDG_DATA_HOME/control-center (else
~/.local/share/control-center) elsewhere. A cwd-relative .cc_server is only
used when no home or app-data directory resolves.
What the server serves
Section titled “What the server serves”cc_server is the whole product surface, not a subset: the repo-RPC data path
and subscriptions, the MCP tool endpoint at POST /mcp and GET /sse on the
same port, inbound webhooks, the media and font proxies, single sign-on
callbacks, the fleet lease protocol, server-side RSS fetching on a 30-minute
schedule and vector search (the sqlite_vector extension is registered as a
process-global auto-extension, degrading to full-text search only where the
extension is unavailable).
The dispatcher is stateless: there is no per-session workspace binding.
Every workspace-scoped call carries its own workspace_id and access is decided
by the caller’s workspace membership and role — not by holding a pairing key.
Two things to know before you expose the port beyond loopback:
- MCP fails closed off-host. A tokenless MCP surface answers loopback only. Binding
anydoes not widen it — an off-host request is refused with “MCP requires a bearer token to answer non-loopback clients” until you configure one (mcp.setToken, or Settings → Server → MCP servers). GET /healthzis unauthenticated by design, with wildcard CORS and publishes the server’s id, name, identity fingerprint and build version. Clients need the fingerprint to pin the server, so this is deliberate — but it does mean any reachablecc_serverdiscloses that much to anyone who asks.