Skip to main content
Most of the app works fine on http://127.0.0.1. iOS device enrollment, over-the-air installs, and the GitHub, GitLab, and Jira webhooks do not: they need a publicly trusted HTTPS origin, because iOS requires a trusted HTTPS callback and the webhook senders have to reach you from the internet. Give the app that origin either by running behind a reverse proxy that sends correct X-Forwarded-Proto and X-Forwarded-Host values, or by setting PUBLIC_BASE_URL to the public HTTPS origin. Direct HTTP localhost and LAN dashboard access stays available, but the enrollment form and profile download are disabled.

Listeners

The server binds two ports, not one. The WebSocket listener carries GraphQL subscriptions: agent job traffic, live build and run output, and the dashboard’s own streaming updates. Control agents dial it outbound, so managed machines and agent containers never expose a listening port of their own. The app also rewrites /graphql on the HTTP origin to the WebSocket listener, and the browser defaults to wss://<page host>/graphql when NEXT_PUBLIC_AGENT_WS_URL is unset. That is what makes a single-origin deployment possible. The production server accepts the standard Next.js HOSTNAME and PORT environment variables plus DATABASE_URL. Set APP_ORIGINS to the public origin users and the OIDC provider use; a request arriving on an origin missing from it is then rejected. It is optional — unset, the server trusts each request’s own host — but setting it is what pins the absolute URLs this server hands to other systems. See Environment variables.

How the public origin is resolved

Several features need to know the address an outside device would use to reach this server. That is resolved per request, in this order: The resolved origin is classified as secure (HTTPS) and as loopback or not. Loopback covers localhost, 127.0.0.0/8, ::1, .local and .localhost names, and the 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 private ranges. Features that require publicly trusted HTTPS check both flags and disable themselves rather than minting a URL a device cannot use.
A malformed PUBLIC_BASE_URL falls through to the request headers instead of throwing, so a typo shows up as a greyed-out enrollment button rather than a 500. If enrollment is unexpectedly unavailable, check the value first.
Hosts carrying whitespace, a slash, or a backslash are rejected. The origin is interpolated into an XML property list for iOS, so a header that could break out of the URL never reaches it.

Reverse proxies

A proxy in front of the app needs to do three things:
1

Terminate TLS with a publicly trusted certificate

A self-signed certificate is enough for the dashboard but not for iOS enrollment or over-the-air installs — iOS will refuse the callback.
2

Forward the original scheme and host

Send X-Forwarded-Proto and X-Forwarded-Host. If your proxy cannot, set PUBLIC_BASE_URL instead.
3

Proxy the WebSocket upgrade on /graphql

Route /graphql to the agent WebSocket port, upgrade headers intact. Without it the dashboard loads but never updates, and agents cannot connect through the proxy.
Give the WebSocket location a long read timeout. Subscriptions are idle between events, and a proxy that closes them after the default minute produces a dashboard that silently stops updating.
Only set NEXT_PUBLIC_AGENT_WS_URL when the browser genuinely cannot reach subscriptions at /graphql on the page’s own origin. It is baked into the client bundle at build time, and on an HTTPS page a configured value that is not wss:// is ignored in favour of the same-origin URL.
Agents are configured separately, with --server and their own WebSocket address at enrollment time. If the control plane sits behind Cloudflare Access, pass each service-token header to the agent with a repeatable --header "Name: value" argument.

Cloudflare Access paths

When the dashboard sits behind Cloudflare Access, its external sign-in layer must not intercept the routes that perform the application’s own authentication or receive integrations. Create more-specific Bypass / Everyone applications for this allowlist:
  • Localized sign-in and registration pages
  • /api/auth/*
  • /api/public/*
  • /api/openapi.json
  • /api/telemetry/analytics-events
  • /api/telemetry/console-logs
  • /api/ios/apns-devices
Configure paths without query strings, because Access path matching does not support one. Better Auth protects its own session and management routes. The integration routes retain their signature, token, validation, and rate-limit checks. That namespace contains only intentionally unauthenticated endpoints: All webhook routes authenticate the raw body before doing any work. GitHub uses X-Hub-Signature-256, Jira uses X-Hub-Signature, and GitLab Standard Webhooks use webhook-id, webhook-timestamp, and webhook-signature with a five-minute replay window. Deliveries are logged on the GitHub webhooks, GitLab webhooks, and Jira webhooks pages.
Do not bypass dashboard pages, /api/graphql, /api/mcp, /api/tools/*, /api/codebases/*, /api/ios/notification-devices, /api/ios/enrollment/start, or /api/ios/devices/export.tsv. Those require Better Auth or their documented agent credential.
Access applications are path-scoped rather than method-scoped; the route handlers themselves expose only the methods listed above. Avoid JavaScript or CAPTCHA challenges on the callback. Add a WAF skip for the exact /api/public/ios/profile-response path only if production logs show that a managed rule blocks genuine iOS callbacks.

Artifact downloads

Over-the-air installs are the reason the artifact routes are public. iOS fetches the manifest, then re-reads the package several times, and it will not present an Access login while doing so. Install manifests mint expiring links: the artifact ID and an expiry are signed with HMAC-SHA256 and travel as token and expires query parameters, valid for six hours. A link is only rejected when a token is supplied and fails verification — the plain links the dashboard renders carry none and are protected by the dashboard’s own authentication instead. The signing key is derived from APP_SECRET, so outstanding links survive a restart. Rotating APP_SECRET invalidates them; they expire in six hours regardless. Artifact responses are sent with cache-control: private, no-store and support HTTP range requests, which is how a resumed or interrupted iOS download recovers.

Client IP observation

Cloudflare Tunnel keeps the origin private, so IP observations trust headers in this order:
  1. A valid CF-Connecting-IP.
  2. The first valid X-Forwarded-For entry.
  3. X-Real-IP.
Each candidate is parsed and validated as an IP address — bracketed IPv6, an appended port, and IPv4-mapped IPv6 forms are all normalized — and an entry that is not a valid address is skipped rather than stored. The selected header source is saved with every observation, so the device detail page shows whether an address came from Cloudflare, a forwarding proxy, or a real-IP header. In direct mode this same behavior assumes the existing trusted localhost/LAN deployment boundary.
These headers are trusted as sent. Only expose the server through a proxy that overwrites them, or a client can claim any address it likes.

Security checklist

Do not expose an unprotected origin to untrusted networks. Publishing the server publishes every route, not just build artifacts.
  • Use only the documented Better Auth and integration public-route allowlist.
  • Set and back up a stable APP_SECRET. It signs sessions, encrypts stored credentials, and signs install links.
  • List every hostname this server is reached at in APP_ORIGINS, and use exact hosts — wildcards are rejected in production because they widen both the CSRF check and the post-login redirect allowlist. Leaving it unset is supported but lets a forged Host steer the URLs this server generates.
  • Create an aide_ API key for every GraphQL or MCP client. Never bypass those endpoints.
  • Enable TRUST_PROXY_HEADERS only when a proxy in front of this server sets the forwarded headers and strips client-supplied copies.
  • Leave APOLLO_SANDBOX unset in production; it enables schema introspection and the Apollo sandbox.
  • Remember that the database file is as sensitive as the secrets in it — see Database.

APIs

What each endpoint does and how it authenticates.

Devices

iOS enrollment, the flow these public paths serve.

Database

Where state lives and how to back it up.

Quickstart

Installing, enrolling an agent, and credential storage.

Docker

Production images, Compose deployment, volumes, and container-agent enrollment.