Required production settings
Set a stable secret before starting a production server. Naming the origins the server is reached at is optional but recommended:APP_SECRET once and back it up. It must be base64 or hex encoding of exactly 32 random bytes; a passphrase is rejected, because the same root also encrypts stored credentials. Changing it signs every user out, and stored credentials become unreadable unless the old value is supplied as APP_SECRET_PREVIOUS — see Environment variables.
APP_ORIGINS lists every origin users and the identity provider use to reach the server, comma-separated. It is what the CSRF check and the post-login redirect check are validated against, so an origin missing from it is rejected with Invalid origin.
Leaving it unset is supported: the server then trusts whatever host each request arrived on, sign-in works on any hostname, and cross-site requests are still rejected. What you give up is control over the absolute URLs the server generates — the OAuth redirect_uri among them — which a forged Host header can otherwise steer. Set it in production whenever you know the hostname. See Environment variables.
When external systems reach this server at a different address than the dashboard — the iOS enrollment flow, over-the-air installs, the GitHub App webhook — set PUBLIC_BASE_URL to that public origin as well. It is trusted automatically without being repeated in APP_ORIGINS.
AUTH_MODE accepts:
The default is
password. The server enforces the mode on its routes. Hiding a form in the browser does not enable a disabled sign-in method. Password accounts do not require email confirmation.
Create the first account
On an empty database, open/register. The first successful account creation closes self-registration automatically. A short-lived database lease makes this atomic when two people submit the setup form at the same time.

/sign-in offers whichever methods AUTH_MODE enables.

Existing OIDC identities can sign in while registration is disabled. A new
OIDC identity is a registration and follows the same toggle as password
signup.
Configure OAuth/OIDC
Set the provider metadata whenAUTH_MODE is oidc or both:
AUTH_OAUTH_DISCOVERY_URL with all three explicit endpoints:
AUTH_OAUTH_REQUIRE_ISSUER_VALIDATION, which defaults to false; set it to true only when the provider includes and validates the OAuth iss response parameter. The provider must return an email address.
The native iOS flow adds a separate S256 PKCE exchange between the app and this server. It does not change the provider registration or the provider redirect URL above:
- The app creates independent 32-byte random
stateandcode_verifiervalues and sendsstate, the S256code_challenge, andcode_challenge_method=S256toGET /api/auth/mobile/oauth/start. - The server keeps its own independent HttpOnly browser-flow nonce while Better Auth completes the PKCE-protected provider exchange.
- The
aide-auth://callbackdeep link returns only an opaquecodeand the app’sstate. An unsuccessful authorization returns a sanitized OAutherror, optionalerror_description, and the samestate. The callback never contains a Better Auth session or one-time token. - After validating the callback scheme, host, and state, the app sends
{"code":"…","code_verifier":"…"}toPOST /api/auth/mobile/redeem. - The server validates the S256 binding and returns the Better Auth bearer session. The authorization code expires after one minute and can be consumed only once; an expired, replayed, or mismatched exchange returns
INVALID_AUTHORIZATION_CODE.
ASWebAuthenticationSession. No additional environment variables are required for either PKCE exchange.
The server does not link a new OIDC identity to a password account solely because their email addresses match. It rejects the ambiguous signup instead. This avoids assigning an external identity to an existing account without a dedicated linking flow.
Credential behavior
Browser sessions use the Better Auth cookie. Native clients send the Better Auth session in
Authorization: Bearer <session-token>. Existing agents use that same header with their agent_ token. API keys use only X-API-Key and begin with aide_.
Invalid or conflicting credentials fail with 401. The server never falls back to anonymous access after receiving a bad credential.
Public routes
Only these application routes should bypass a reverse proxy’s sign-in layer:- Localized
/sign-inand/registerpages, plus/api/auth/*callbacks and mobile completion routes /api/public/*, whose webhook signatures and short-lived artifact tokens remain enforced/api/openapi.json/api/telemetry/analytics-eventsand/api/telemetry/console-logs- Third-party app registration at
/api/ios/apns-devices
/api/graphql, /api/mcp, first-party /api/ios/notification-devices, enrollment start and export routes, Tools REST, and every other control-plane route protected. GraphQL performs its own credential checks before parsing or resolving operations, so aliases, batches, introspection, and mixed operations cannot bypass the enrollment exception.
iOS sessions
The iOS app fetches/api/auth/config for the selected server before it initializes the dashboard. It stores a bearer session in Keychain, scoped to that server origin. Password sign-in stays native. OIDC opens an ASWebAuthenticationSession, returns through the aide-auth URL scheme with an opaque authorization code, and redeems that code with the in-memory PKCE verifier. The one-minute code is single-use and is not itself a Better Auth session or token.
Signing out invalidates the server session, removes the Keychain item, clears Apollo’s cache, closes subscriptions, and defers first-party push registration until the next sign-in.
Upgrade from the MCP environment token
TOOLS_API_TOKEN is no longer supported. Unscoped and preset MCP endpoints no longer permit anonymous access or an environment-token fallback.
- Sign in and open API keys.
- Create a key and copy it from the one-time result.
- Remove
TOOLS_API_TOKENfrom the server environment. - Replace
Authorization: Bearer <old-token>in MCP clients withX-API-Key: aide_....
Authorization: Bearer agent_... credential.
