Skip to content

Environment variables reference

This page lists every environment variable read by the Polaris Express stack. Values come from web/.env.example; if you see a drift between this page and that file, the file wins — open an issue and we’ll re-sync.

Variables are grouped by subsystem. Within each group, “Required” means the service will refuse to boot or will misbehave without it. “Optional” means the default is sane and you can ignore the variable until you need to tune it.

The web service (web/) reads .env at boot via Deno’s standard env loader. Docker Compose injects the same file into the steve and postgres containers where needed. The Cloudflare Email Worker and APNs paths have their own secrets stored separately (see the relevant phases).

NameDefaultRequiredNotes
DATABASE_URLpostgresql://user:password@localhost:5432/ocpp_billingYesFull connection string used by the web service.
POSTGRES_DBocpp_billingYesDatabase name created by the Postgres container.
POSTGRES_USERocpp_userYesSuperuser for the database.
POSTGRES_PASSWORDocpp_passwordYesChange before any non-local deploy.
NameDefaultRequiredNotes
STEVE_BASE_URLhttp://localhost:8080/steveYesBase URL for dashboard links. API calls append /api.
STEVE_API_USERNAMEadminYesMust match SteVe’s auth.user.
STEVE_API_KEY(empty)YesMust match SteVe’s webapi.value. HTTP Basic auth since SteVe 3.8.
DOCKER_SOCKET_PATH/var/run/docker.sockYesUsed to stream SteVe container logs for tag detection.
STEVE_CONTAINER_NAMEsteveYesContainer name to stream from.

See the SteVe phase for how these values are generated on the SteVe side.

NameDefaultRequiredNotes
STEVE_PREAUTH_HMAC_KEY(empty)Yes (Wave 1+)HMAC shared secret for pre-authorize webhook. Generate with openssl rand -hex 32.
STEVE_METERVALUE_HMAC_KEY(empty)Yes (Wave 1+)HMAC shared secret for MeterValues webhook.
NameDefaultRequiredNotes
LAGO_API_URLhttps://api.getlago.comYesAPI base. The client appends /api/v1.
LAGO_API_KEY(empty)YesLago API key with org scope.
LAGO_DASHBOARD_URLhttps://app.getlago.comYesUsed for “Open in Lago” links from the admin console.
LAGO_METRIC_CODEev_charging_kwhYesCode of the Lago billable metric for kWh consumption. Must exist in Lago before first sync.
NameDefaultRequiredNotes
PORT8000NoPort the web service binds. Change in tandem with your reverse proxy.
DENO_ENVdevelopmentNoSet to production for production deploys. Enables stricter error handling and disables verbose tracebacks.
DEBUG_LEVELERRORNoOne of ERROR, WARN, INFO, DEBUG. Use INFO when debugging a live incident.

The sync worker runs on adaptive cadence by default: Active charge boxes poll every 15 minutes, Idle every hour, Dormant once a week.

NameDefaultRequiredNotes
SYNC_CRON_SCHEDULE(unset)NoEscape hatch. If set to a cron pattern, all tiers use that pattern and adaptive behavior is disabled. Leave unset in normal operation.
SYNC_ON_STARTUPfalseNoIf true, runs a sync tick immediately on worker boot. Useful for testing; leave false in production to avoid thundering-herd on restart.
SYNC_LOOKBACK_MINUTES10080NoHow far back to scan SteVe transactions each tick. Default is 7 days. Reduce to 1440 (24h) only if you’re certain the worker cannot lag.
SYNC_DORMANT_THRESHOLD_DAYS30NoDays of no activity before a charge box is demoted from Idle to Dormant.
SYNC_IDLE_HYSTERESIS_TICKS2NoConsecutive idle evaluations required to demote a tier. Higher = more conservative.
NameDefaultRequiredNotes
AUTH_SECRET(empty)YesMinimum 32 characters. Generate with openssl rand -hex 32. Rotating this invalidates every active session.
AUTH_URLhttp://localhost:8000YesCanonical customer-portal URL. Used as session issuer and for magic-link composition.
NameDefaultRequiredNotes
CF_EMAIL_WORKER_URL(empty)NoURL of the Cloudflare Email Worker. Leave empty in dev to log emails to console.
CF_EMAIL_WORKER_SECRET(empty)Yes (with Worker)32-byte shared secret. Required only when CF_EMAIL_WORKER_URL is set.
EMAIL_FROM(empty)YesFrom: header for customer-facing email, e.g. Polaris Express <[email protected]>.
EMAIL_FROM_ADMIN(empty)YesFrom: header for admin-facing email. Use a distinct mailbox so admins can filter.
OPERATOR_CONTACT_EMAIL(empty)Yesmailto: target shown to inactive customers and on hard-error pages.

All values in seconds.

NameDefaultRequiredNotes
MAGIC_LINK_TTL_SECONDS900NoStandard 15-minute customer magic link.
MAGIC_LINK_INVITE_TTL_SECONDS86400No24-hour first-time invite link.
ADMIN_PASSWORD_RESET_TTL_SECONDS86400No24-hour admin password-reset link.
CUSTOMER_SESSION_TTL_SECONDS28800No8-hour ceiling for customer sessions.
NameDefaultRequiredNotes
ADMIN_BASE_URL(empty)YesCanonical admin host, e.g. https://manage.example.com. Used for admin redirects and password-reset emails.
CUSTOMER_BASE_URL(empty)YesCanonical customer host, e.g. https://example.com.
COOKIE_DOMAIN(empty)YesCookie scope, e.g. .example.com (note leading dot). Must cover both admin and customer subdomains so sessions work on either surface.

When set, the admin login presents an OIDC button. When unset, login falls back to email/password. See the admin OIDC phase.

NameDefaultRequiredNotes
ADMIN_OIDC_ISSUER(unset)NoOIDC issuer URL, e.g. https://pocket-id.example.com. Setting this enables the OIDC plugin.
ADMIN_OIDC_CLIENT_ID(unset)Yes (with OIDC)Client ID registered at the IdP.
ADMIN_OIDC_CLIENT_SECRET(unset)Yes (with OIDC)Client secret from the IdP.
ADMIN_AUTH_SHOW_FALLBACKfalseNoIf true, shows the email/password form alongside the OIDC button as a break-glass.

Token-based JWT (ES256) auth. Missing values do not block boot — push sends return {ok:false,reason:"JwtSignFailed"} and the app falls back to SSE.

NameDefaultRequiredNotes
APNS_KEY_ID(unset)NoKey ID from App Store Connect → Users & Access → Keys.
APNS_TEAM_ID(unset)No10-character Apple Team ID.
APNS_KEY_BASE64(unset)NoBase64-encoded .p8 key contents (including BEGIN/END lines). See below.
APNS_TOPIC(unset)NoiOS bundle identifier, e.g. com.example.expresscharge.ios.

Encode the key file as:

Terminal window
APNS_KEY_BASE64=$(base64 -i AuthKey_ABC123XYZ9.p8 | tr -d '\n')

After editing .env, dry-run the loader:

Terminal window
docker compose config | grep -E '^(\s+)(AUTH_SECRET|LAGO_API_KEY|STEVE_API_KEY)'

You should see your secrets resolved (do not paste this output anywhere). If any value is empty, the corresponding service will fail its boot health check.

For a runtime check, hit the admin health endpoint after starting the stack:

Terminal window
curl -s http://localhost:8000/api/admin/health | jq

The response enumerates which subsystems found their config and which fell back to defaults.

  • Service crashes on boot with “missing AUTH_SECRET”: the value must be at least 32 characters. Regenerate with openssl rand -hex 32.
  • Customer can’t log in across subdomains: check COOKIE_DOMAIN has a leading dot and matches both ADMIN_BASE_URL and CUSTOMER_BASE_URL.
  • Lago sync silently does nothing: confirm LAGO_METRIC_CODE matches a billable metric that already exists in Lago. The sync worker logs at INFO level when the metric is missing.
  • SteVe API calls return 401: STEVE_API_USERNAME and STEVE_API_KEY must exactly match SteVe’s auth.user and webapi.value. Restart the SteVe container after changing them.