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.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.--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
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.
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 astoken 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:- A valid
CF-Connecting-IP. - The first valid
X-Forwarded-Forentry. X-Real-IP.
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
- 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 forgedHoststeer the URLs this server generates. - Create an
aide_API key for every GraphQL or MCP client. Never bypass those endpoints. - Enable
TRUST_PROXY_HEADERSonly when a proxy in front of this server sets the forwarded headers and strips client-supplied copies. - Leave
APOLLO_SANDBOXunset 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.
Related pages
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.