Skip to main content

Secure your instance

A Costpilot instance has two kinds of clients with two kinds of credentials: people sign in to the dashboard with an account, developer machines and pipelines send telemetry with the ingestion token. Demo mode turns accounts off so the public demo stays open.

Accounts for the dashboard​

Outside demo mode the dashboard and /api/* require a sign-in. Accounts live in the same SQLite database (via Better Auth, in-process: no extra service, no mail server).

  • First start: the server log prints a one-time setup code (Auth: no account yet - ... setup code 1a2b3c...). Open the dashboard, enter it and create the first admin. For automated deployments set COSTPILOT_ADMIN_EMAIL and COSTPILOT_ADMIN_PASSWORD instead.
  • Team: admins invite people under Admin → Users. An invite is a single-use link (valid 7 days) that the admin shares directly.
  • Roles: Admin manages users, projects, agent settings, branding and budget alerts; Viewer sees the whole dashboard. The last admin cannot be demoted or removed.
  • Sessions: httpOnly, SameSite=Lax cookies, stored server-side (7 days, extended while in use), revoked on sign-out and removal; a password change revokes the other sessions. Sign-in is only accepted from the dashboard's own origin and rate-limited in the Docker image (production mode).
VariablePurpose
COSTPILOT_PUBLIC_URLPublic dashboard URL, e.g. https://costpilot.example.com. Recommended: pins the trusted origin and cookie security. Needed when serving over plain HTTP on a non-localhost host, otherwise the browser drops the Secure cookie
COSTPILOT_AUTH_SECRETCookie signing secret (32+ random chars). Unset = generated once and stored in the database
COSTPILOT_ADMIN_EMAIL / COSTPILOT_ADMIN_PASSWORDCreates the first admin on startup while no account exists
COSTPILOT_AUTH=offDisables accounts, e.g. behind a reverse proxy that authenticates already. DEMO_MODE=1 implies it

Token for ingestion​

/v1/* (telemetry, session reports, agent config) is never behind the sign-in: the agent has no interactive login. Protect it with a bearer token:

COSTPILOT_INGEST_TOKEN=$(openssl rand -hex 24)

Developers pass it once; the agent writes it into each tool's telemetry headers (OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer <secret>" or the tool's equivalent) and appends --token to the hook command:

costpilot-agent init --endpoint https://costpilot.example.com --token <secret>

/health always stays open for platform healthchecks.

Rotating the token​

Change COSTPILOT_INGEST_TOKEN on the server, redeploy, then have every developer re-run init with the new --token. Until they do, their telemetry is rejected with 401 and costpilot-agent check fails on the server line.

Project-level setups and secrets​

init --project writes the token into files that usually live in the repository: ./.envrc, ./.codex/config.toml, ./.gemini/.env and the hook command in ./.claude/settings.json, ./.gemini/settings.json, ./.codex/hooks.json and ./.cursor/hooks.json. Keep those files out of version control, or provide the token through the environment instead. See Report only some projects.

Upgrading from Basic Auth​

COSTPILOT_BASIC_AUTH from earlier versions is gone. After the update, create the first admin with the setup code (or the admin variables) and invite the team.

Split hosting​

With the dashboard on another origin, set CORS_ORIGINS to that origin explicitly (that also allows the session cookie). Sign-in then needs both on the same site, e.g. app.example.com and api.example.com, because the cookie is SameSite=Lax.