Ga naar inhoud

Een headless server draaien

Draai cc_server op een machine die aan blijft staan en maak er vervolgens verbinding mee vanaf je desktop, een browser of je telefoon. De server is een native binary in puur Dart — geen Flutter-engine — en draait dus headless op macOS, Linux en Windows. Hij bezit de database, draait de agenten en doet elke externe API-aanroep; de clients renderen alleen.

Elke release publiceert een self-hosted stack van vier GHCR-images — de backend, de twee statische thin clients en de optionele koppelingsrelay — plus een aparte cc-server-demo-image voor de openbare demohost. Een self-hosted installatie heeft geen source-checkout nodig. Pin een versietag (ghcr.io/samuelalev/<image>:v0.0.1-rc.1) of blijf op latest.

Image Wat het is Publiceert
ghcr.io/samuelalev/cc-server De backend: database, agenten, MCP, webhooks -p 9030:9030
ghcr.io/samuelalev/cc-webapp De webclient (statische nginx) -p 8080:8080
ghcr.io/samuelalev/cc-remote De telefooncompanion-PWA (statische nginx) -p 8081:8080
ghcr.io/samuelalev/cc-signaling-server De WebRTC-koppelingsrelay (optioneel) -p 8788:8788

De hele stack als composebestand:

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:

Dingen die je moet weten voordat je dit alles buiten loopback beschikbaar stelt:

  • De cc-server-image levert CC_SERVER_INSECURE=1 mee (hij moet 0.0.0.0 binden om verkeer via gepubliceerde poorten te ontvangen), wat alleen veilig is achter een TLS-beëindigende reverse proxy — Caddy, Traefik, nginx of een Cloudflare Tunnel — op een vertrouwd netwerk. Wil je TLS juist in-process beëindigen, mount dan een certificaat en stel CC_SERVER_TLS_CERT / CC_SERVER_TLS_KEY in (haal daarna CC_SERVER_INSECURE weg). Alles is te overschrijven met docker run -e … of het environment:-blok van compose.

  • /data is de enige state. De SQLite-databases, secrets van gekoppelde apparaten, gedownloade modellen en vergaderaudio staan allemaal in het volume — maak er een back-up van en de container is wegwerpbaar.

  • Koppel een client vanuit de image zelf vóór de eerste verbinding, het container-equivalent van stap 3:

    Terminalvenster
    docker run --rm -it -v cc_data:/data \
    --entrypoint /app/bin/cc_server ghcr.io/samuelalev/cc-server:latest pair
  • De twee client-images zijn statische nginx-hosts — niets om tijdens runtime te configureren; het verbindingsformulier van elke client neemt je server-URL, apparaat-id en koppelingssleutel. De signaleringsrelay heeft net zo goed alleen zijn poort nodig (SIGNALING_HOST / SIGNALING_PORT zijn te overschrijven); hij is stateless, bevat geen secrets en is een fallback — een telefoon gebruikt loopback, LAN, tailnet of een direct wss://-pad wanneer dat bereikbaar is, en valt alleen terug op de relay als geen daarvan werkt. Sla hem over als je clients de server altijd rechtstreeks bereiken.

De rest van deze gids bouwt de server uit de source; de flags en het gedrag die erin aan bod komen, zijn dezelfde als waar de omgevingsvariabelen van de image op uitkomen.

  • De Dart SDK. Deze repo pint zijn SDK met fvm, dus zet fvm voor elk commando.
  • Deze repository uitgecheckt.
  • De native toolchain die het staging-script nodig heeft — Rust/cargo, plus meson, ninja, pkg-config, git en een C++-compiler. Elk buildscript noemt wat het niet kan vinden en hoe je het installeert.
  • Een bereikbare host: loopback voor lokaal gebruik, of een LAN-/openbaar adres met TLS voor externe clients.

De natives zijn verplicht — er is geen degraded mode. Bouw ze als eerste vanuit de root van de repo:

Terminalvenster
scripts/natives/build_natives.sh

Dit bouwt rift, fff, tree-sitter plus de grammatica’s, aec, lame, ccpty, cc_watcher, cc_inference en de SAML-native naar build/natives/. De buildhook van cc_server neemt alles wat daar is klaargezet opnieuw op in de bundle, dus als je deze stap overslaat mislukt de build of — met de noodoplossing hieronder — levert hij een binary op waarvan de boot-preflight weigert te starten en de ontbrekende library noemt.

Terminalvenster
cd apps/cc_server
fvm dart build cli

De bundle levert libsqlite3 en de klaargezette natives mee naast de binary — geen systeem-SQLite en geen Flutter-engine nodig. Het uitvoerbare bestand komt terecht in ./build/cli/<os_arch>/bundle/bin/cc_server.

Stap 3: Een client koppelen vóór de eerste start

Sectie getiteld “Stap 3: Een client koppelen vóór de eerste start”

Een verse datadirectory heeft geen gekoppeld apparaat en geen vooraf gedeelde sleutel, dus de prompt om een koppelingssleutel in een client kan nog niet worden vervuld. Maak er een aan met het subcommando pair. Het schrijft rechtstreeks naar global.db en secrets.json; een server die die datadir al bedient, neemt het nieuwe apparaat over zonder herstart:

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

Hij drukt drie waarden af die je in het verbindingsformulier van een client plakt:

Connect a thin client (paste into its connect form):
Server ws://localhost:9030/rpc
Device id web-client
Pairing key <a freshly generated key>

Nuttige flags: --device (standaard web-client; het id bepaalt ook de apparaattier — desktop, ios, android, al het andere is een webclient), --label (standaard de platformnaam plus de hostnaam van deze machine), --host (de host die in de URL wordt afgedrukt, want loopback is niet bereikbaar vanaf een andere machine) en --client-url. Als je pair opnieuw uitvoert, roteert de sleutel van dat apparaat en vervallen de live-sessies. Koppelen maakt nooit een werkruimte aan: de onboarding van de verbindende client benoemt en maakt de eerste.

Met --client-url drukt hij ook een QR af die je in de terminal kunt scannen en die de webclient al ingevuld opent:

Terminalvenster
./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

Een referentie die door cc_server pair is aangemaakt, heeft geen vervaldatum; een referentie die in de app is aangemaakt, verloopt altijd na 30 dagen. Trek een via de CLI aangemaakt apparaat in via Instellingen → Jij → Je apparaten wanneer je het niet meer nodig hebt.

Op loopback — de standaard en alles wat je nodig hebt voor een client op dezelfde machine:

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

Om hem vanaf een andere machine te bereiken, bind je elke interface. Een niet-loopbackbind zonder TLS wordt bij het opstarten geweigerd (Refusing to bind non-loopback address … without TLS), dus geef een certificaat mee:

Terminalvenster
./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

Of beëindig TLS in een reverse proxy op een vertrouwd privénetwerk en kies voor een plaintextbind erachter:

Terminalvenster
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030 \
--bind any --insecure --public-url wss://server.example/rpc

--insecure wordt genegeerd wanneer --tls-cert en --tls-key beide zijn ingesteld — TLS wint altijd. De server adverteert de plaintext-state als insecure: true in GET /healthz en clients tonen zo’n verbinding met de badge Onveilig.

De server blijft draaien tot SIGINT/SIGTERM en sluit dan netjes af.

Wijs een client naar de server-URL en koppelingssleutel uit stap 3:

  • Desktop-app: vul op het eerste scherm (Hoe moet Control Center draaien?) de verbindingsvelden in in plaats van In deze app uitvoeren, of voeg de server later toe via Instellingen → Server → Verbinding en status. Zie Verbinden met een externe server.
  • Webclient: open app.usectrl.dev (of je eigen kopie van de webbuild) en voer de server-URL, het apparaat-id en de koppelingssleutel in.
  • Telefoon: koppel de Remote-app vanuit een draaiende client — zie Een apparaat koppelen.

Stap 6: Een Google Calendar verbinden (optioneel)

Sectie getiteld “Stap 6: Een Google Calendar verbinden (optioneel)”

Gebruik de devicecode-flow om de agenda van een werkruimte te synchroniseren zonder GUI:

Terminalvenster
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>

Hij drukt een code en een URL af om op een ander apparaat goed te keuren, slaat het refreshtoken server-side op en stopt. Een build uit de source levert geen Google-client mee, dus de omgevingsvariabelen voor id en secret zijn verplicht — er is geen --google-client-id-flag. Zie Een Google Calendar verbinden.

De flags hierboven zijn de flags die een eerste setup nodig heeft. Het volledige oppervlak — 23 paren van flags en omgevingsvariabelen, de vier subcommando’s en de indeling van de datadirectory — staat in de cc_server CLI-referentie.

De vier die je het waarschijnlijkst nodig hebt:

Flag Standaard Betekenis
--data-dir de app-gegevensmap per gebruiker van het OS Databases, secrets, modellen en gecachte media
--port 9030 TCP-poort (0 = kortstondig)
--bind loopback loopback, of any voor elke interface (vereist TLS of --insecure)
--public-url afgeleid van de bind De RPC-URL die aan gekoppelde clients wordt geadverteerd. Stel deze expliciet in achter een proxy, NAT of tunnel

De standaard datadir is ~/Library/Application Support/control-center op macOS, %APPDATA%\control-center op Windows en $XDG_DATA_HOME/control-center (anders ~/.local/share/control-center) elders. Een .cc_server relatief aan de cwd wordt alleen gebruikt als er geen home- of app-gegevensmap kan worden bepaald.

cc_server is het volledige productoppervlak, geen subset: het repo-RPC- gegevenspad en de abonnementen, het MCP-tool-endpoint op POST /mcp en GET /sse op dezelfde poort, inkomende webhooks, de media- en font-proxy’s, callbacks voor eenmalige aanmelding, het fleet-leaseprotocol, server-side RSS-fetching volgens een schema van 30 minuten en vectorsearch (de sqlite_vector-extensie wordt geregistreerd als proces-globale auto-extensie en degradeert alleen naar full-text search waar de extensie niet beschikbaar is).

De dispatcher is stateless: er is geen werkruimtebinding per sessie. Elke aanroep met werkruimtescope draagt zijn eigen workspace_id mee en toegang wordt bepaald door het lidmaatschap en de rol van de aanroeper in de werkruimte — niet door het bezit van een koppelingssleutel.

Twee dingen die je moet weten voordat je de poort buiten loopback beschikbaar stelt:

  • MCP faalt gesloten buiten de host. Een MCP-oppervlak zonder token antwoordt alleen op loopback. any binden verruimt dat niet — een verzoek van buiten de host wordt geweigerd met “MCP requires a bearer token to answer non-loopback clients” tot je er een configureert (mcp.setToken, of Instellingen → Server → MCP-servers).
  • GET /healthz is bewust niet geauthenticeerd, met wildcard-CORS, en publiceert het id, de naam, de identiteitsvingerafdruk en de buildversie van de server. Clients hebben de vingerafdruk nodig om de server te pinnen, dus dit is opzettelijk — maar het betekent wel dat elke bereikbare cc_server dat alles prijsgeeft aan wie er maar om vraagt.