Phase 1 — Generate credentials
This phase is purely local: you generate every secret, key, and shared
HMAC that the rest of the install will need. Doing this up front means
you can paste values into .env files as you go, rather than stopping
mid-install to mint a random string.
Phase 1 produces credentials only. Nothing is deployed yet, no DNS is configured, and no external accounts (Cloudflare, Apple, Pocket ID) are touched here — those come in later phases.
Prerequisites
Section titled “Prerequisites”- Phase 0 complete — you have the repo cloned and a
web/.envfile copied fromweb/.env.example. opensslavailable on the host you’re running this from (any recent Linux or macOS shell qualifies).- A password manager or secret store you trust. Treat this phase’s output the same way you’d treat a production database password — because some of it is a production database password.
-
Generate the Postgres password
Pick a long, random password for the application database. This goes into both
POSTGRES_PASSWORDand the password segment ofDATABASE_URL.Terminal window openssl rand -base64 32Paste the result into
web/.env:web/.env POSTGRES_USER=ocpp_userPOSTGRES_PASSWORD=<paste-here>POSTGRES_DB=ocpp_billingDATABASE_URL=postgresql://ocpp_user:<paste-here>@postgres:5432/ocpp_billing -
Generate the BetterAuth session secret
BetterAuth signs session cookies and magic-link tokens with
AUTH_SECRETrequired . It must be at least 32 bytes of real entropy.Terminal window openssl rand -base64 48web/.env AUTH_SECRET=<paste-here> -
Generate the SteVe API key
SteVe authenticates REST API calls with HTTP Basic auth (since SteVe 3.8). The username matches SteVe’s
auth.user; the API key matcheswebapi.value. You’ll wire both sides up in the SteVe phase — for now just generate the key:Terminal window openssl rand -hex 32web/.env STEVE_API_USERNAME=adminSTEVE_API_KEY=<paste-here>Keep this value somewhere you can retrieve it — you’ll paste the same string into SteVe’s
main.propertieslater. -
Generate the Email Worker shared secret
The Polaris Express web app and the Cloudflare Email Worker authenticate to each other by HMAC. The Worker calls it
POLARIS_SECRET_A; the web app calls itCF_EMAIL_WORKER_SECRET. They must hold the same value.Terminal window openssl rand -base64 32web/.env CF_EMAIL_WORKER_SECRET=<paste-here>Save the same value to your secret store under a name like
polaris/email-worker/POLARIS_SECRET_A. You’ll need it again when you deploy the Worker (see the Email Worker phase) vianpx wrangler secret put POLARIS_SECRET_A. -
Generate the SteVe webhook HMAC keys
SteVe posts pre-authorize and meter-value webhooks to ExpresSync. Each webhook channel has its own HMAC secret so you can rotate them independently:
Terminal window # STEVE_PREAUTH_HMAC_KEYopenssl rand -hex 32# STEVE_METERVALUE_HMAC_KEYopenssl rand -hex 32web/.env STEVE_PREAUTH_HMAC_KEY=<paste-first-value>STEVE_METERVALUE_HMAC_KEY=<paste-second-value>You’ll paste the same two values into SteVe’s configuration in the SteVe phase.
-
Record the Lago API key
Lago is the only credential in this phase that you don’t generate yourself — you copy it from a Lago instance.
- Lago Cloud: log in at
app.getlago.com→ Developers → API keys. - Self-hosted Lago: copy the API key from your Lago install’s admin UI.
web/.env LAGO_API_URL=https://api.getlago.comLAGO_DASHBOARD_URL=https://app.getlago.comLAGO_API_KEY=<paste-from-lago>LAGO_METRIC_CODE=ev_charging_kwh - Lago Cloud: log in at
-
Stage the optional credentials
These don’t need values yet, but it’s worth noting now whether you plan to use them so you can leave the corresponding lines commented in
.env:- Pocket ID OIDC (admin SSO): generate the client secret in your
Pocket ID instance during the admin-auth phase. Leave
ADMIN_OIDC_CLIENT_SECRETcommented for now. - APNs (iOS push, Wave 2): you need an Apple Developer account and
a
.p8key from App Store Connect. Skip if you’re not deploying the iOS app.APNS_KEY_BASE64,APNS_KEY_ID,APNS_TEAM_ID, andAPNS_TOPICcan all stay unset — the push send will just return{ok:false,reason:"JwtSignFailed"}and the SSE fallback takes over.
- Pocket ID OIDC (admin SSO): generate the client secret in your
Pocket ID instance during the admin-auth phase. Leave
-
Back up your `.env` file
Copy the completed
web/.envto your password manager or secret store. If you lose it, you cannot recover existing customer sessions, magic-link tokens in flight, or in-progress sync state without regenerating values and reconciling by hand.Terminal window # Verify the file is gitignored before doing anything else.cd polaris-expressgit check-ignore -v web/.env# Expected: web/.gitignore prints, confirming the file is ignored.
Configure environment variables
Section titled “Configure environment variables”This is the full list of secrets you should have populated by the end of Phase 1. Hostnames, URLs, and TTLs come in later phases — only secret material is in scope here.
| Name | Source | Required | Notes |
|---|---|---|---|
POSTGRES_PASSWORD | openssl rand | Yes | Embedded in DATABASE_URL as well |
DATABASE_URL | Composed | Yes | postgresql://user:pass@host:5432/db |
AUTH_SECRET | openssl rand | Yes | ≥ 32 chars; rotating logs every user out |
STEVE_API_KEY | openssl rand | Yes | Mirrored into SteVe’s main.properties |
CF_EMAIL_WORKER_SECRET | openssl rand | Yes | Mirrored into Worker as POLARIS_SECRET_A |
STEVE_PREAUTH_HMAC_KEY | openssl rand | Yes | Mirrored into SteVe’s pre-auth webhook config |
STEVE_METERVALUE_HMAC_KEY | openssl rand | Yes | Mirrored into SteVe’s meter-value webhook config |
LAGO_API_KEY | Lago dashboard | Yes | From Lago → Developers → API keys |
ADMIN_OIDC_CLIENT_SECRET | Pocket ID | Optional | Only if you’re using Pocket ID admin SSO |
APNS_KEY_BASE64 | App Store Connect | Optional | Only if shipping the iOS app |
Verify
Section titled “Verify”You can’t curl anything in Phase 1 — nothing’s running yet. Verify
locally instead:
# Every required secret should be at least 32 characters.cd webgrep -E '^(POSTGRES_PASSWORD|AUTH_SECRET|STEVE_API_KEY|CF_EMAIL_WORKER_SECRET|STEVE_PREAUTH_HMAC_KEY|STEVE_METERVALUE_HMAC_KEY|LAGO_API_KEY)=' .env \ | awk -F= '{ if (length($2) < 32) print "TOO SHORT: " $1 " (" length($2) " chars)" }'Expected output: nothing. Anything printed means that secret is shorter than 32 characters and should be regenerated.
Also confirm:
.envdoes not appear ingit status.git check-ignore web/.envconfirms it’s ignored.- Your secret-store entries match the values in
.envexactly (whitespace and all — a trailing newline is a real bug here).
If something goes wrong
Section titled “If something goes wrong”openssl: command not found
Install OpenSSL (apt install openssl, brew install openssl, or
your distro’s equivalent). On Windows, use WSL or Git Bash.
You committed .env to git.
Treat every secret in that file as compromised. Regenerate all of
them, force-push the history rewrite, and rotate the Lago API key
from Lago’s UI. Cleaning the commit out of origin is not enough —
assume mirrors exist.
You lost your .env and have nothing running yet.
Easy: regenerate the file from scratch. No data has been written
anywhere, so there’s nothing to reconcile.
You lost your .env and services are already deployed.
Stop. Do not regenerate AUTH_SECRET blindly — it will log out every
customer and invalidate every magic link. Recover from your secret
store if you can. If you can’t, plan a maintenance window: rotate
each secret one at a time, redeploying the dependent service
(database, SteVe, Email Worker, Lago) between rotations.
Next phase
Section titled “Next phase”Continue to Phase 2 — Provision DNS and TLS
to set up the hostnames you’ll bind to AUTH_URL ,
ADMIN_BASE_URL , and COOKIE_DOMAIN
in later phases.