Gå til innholdet

Run a headless server

Dette innholdet er ikke tilgjengelig på ditt språk ennå.

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.

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-server image ships CC_SERVER_INSECURE=1 (it must bind 0.0.0.0 to 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 set CC_SERVER_TLS_CERT / CC_SERVER_TLS_KEY (then unset CC_SERVER_INSECURE). Everything is overridable with docker run -e … or the compose environment: block.

  • /data is 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_PORT overridable); it is stateless, holds no secrets and is a fallback — a phone uses loopback, LAN, tailnet or a direct wss:// 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.

  • 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, plus meson, ninja, pkg-config, git and 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.

The natives are required — there is no degraded mode. Build them from the repo root before anything else:

Terminal window
scripts/natives/build_natives.sh

This 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.

Terminal window
cd apps/cc_server
fvm dart build cli

The 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.

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:

Terminal window
./build/cli/<os_arch>/bundle/bin/cc_server pair --data-dir ./data --port 9030

It 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:

Terminal window
./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.dev

A 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.

On loopback — the default and all you need for a client on the same machine:

Terminal window
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030

To 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:

Terminal window
./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/rpc

Or terminate TLS in a reverse proxy on a trusted private network and opt into a plaintext bind behind it:

Terminal window
./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.

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:

Terminal window
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.

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.

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 any does 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 /healthz is 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 reachable cc_server discloses that much to anyone who asks.