Server deployment
appctl serve is an HTTP + WebSocket daemon. Any client that can send an HTTP request or open a WebSocket can talk to it.
Minimum viable deployment
Section titled “Minimum viable deployment”appctl serve \ --bind 0.0.0.0 \ --port 4242 \ --token "$(openssl rand -hex 32)" \ --strict \ --confirmPut TLS termination in front (Caddy, Nginx, Cloudflare Tunnel). appctl serve does not terminate TLS.
systemd unit
Section titled “systemd unit”/etc/systemd/system/appctl.service:
[Unit]Description=appctl serveAfter=network-online.target
[Service]Type=simpleUser=appctlWorkingDirectory=/srv/appctlEnvironment="APPCTL_TOKEN=replace-me"ExecStart=/usr/local/bin/appctl serve \ --bind 127.0.0.1 \ --port 4242 \ --token ${APPCTL_TOKEN} \ --strictRestart=on-failureRestartSec=5s
[Install]WantedBy=multi-user.targetReload and start:
sudo systemctl daemon-reloadsudo systemctl enable --now appctlCaddy in front
Section titled “Caddy in front”appctl.internal.example.com { encode zstd gzip reverse_proxy 127.0.0.1:4242}Caddy handles TLS. appctl stays on loopback.
Embed in a product
Section titled “Embed in a product”// POST /run from a browser or serverconst res = await fetch("https://appctl.internal/run", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, }, body: JSON.stringify({ message: "show me the 5 latest orders" }),});const { result, events } = await res.json();For streaming UIs, open a WebSocket to /chat instead. See WebSocket.
Safety posture
Section titled “Safety posture”For any network beyond localhost:
- Always set
--token. - Default to
--strict. Only allowprovenance=verifiedtools. - Consider
--read-onlyfor the main deployment and a separate write-enabled instance on a different port or host. - Run under a dedicated low-privilege user (
appctlabove).
Scaling
Section titled “Scaling”appctl serve is a single process with in-memory state. For redundancy, front multiple instances with a sticky-session load balancer (WebSocket connections must land on the same instance). Shared state is limited to the SQLite audit log; point multiple instances at different --app-dir directories.