Skip to content

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.

  • Phase 0 complete — you have the repo cloned and a web/.env file copied from web/.env.example.
  • openssl available 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.
  1. Generate the Postgres password

    Pick a long, random password for the application database. This goes into both POSTGRES_PASSWORD and the password segment of DATABASE_URL .

    Terminal window
    openssl rand -base64 32

    Paste the result into web/.env:

    web/.env
    POSTGRES_USER=ocpp_user
    POSTGRES_PASSWORD=<paste-here>
    POSTGRES_DB=ocpp_billing
    DATABASE_URL=postgresql://ocpp_user:<paste-here>@postgres:5432/ocpp_billing
  2. Generate the BetterAuth session secret

    BetterAuth signs session cookies and magic-link tokens with AUTH_SECRET required . It must be at least 32 bytes of real entropy.

    Terminal window
    openssl rand -base64 48
    web/.env
    AUTH_SECRET=<paste-here>
  3. 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 matches webapi.value. You’ll wire both sides up in the SteVe phase — for now just generate the key:

    Terminal window
    openssl rand -hex 32
    web/.env
    STEVE_API_USERNAME=admin
    STEVE_API_KEY=<paste-here>

    Keep this value somewhere you can retrieve it — you’ll paste the same string into SteVe’s main.properties later.

  4. 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 it CF_EMAIL_WORKER_SECRET . They must hold the same value.

    Terminal window
    openssl rand -base64 32
    web/.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) via npx wrangler secret put POLARIS_SECRET_A.

  5. 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_KEY
    openssl rand -hex 32
    # STEVE_METERVALUE_HMAC_KEY
    openssl rand -hex 32
    web/.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.

  6. 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.com
    LAGO_DASHBOARD_URL=https://app.getlago.com
    LAGO_API_KEY=<paste-from-lago>
    LAGO_METRIC_CODE=ev_charging_kwh
  7. 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_SECRET commented for now.
    • APNs (iOS push, Wave 2): you need an Apple Developer account and a .p8 key from App Store Connect. Skip if you’re not deploying the iOS app. APNS_KEY_BASE64 , APNS_KEY_ID , APNS_TEAM_ID , and APNS_TOPIC can all stay unset — the push send will just return {ok:false,reason:"JwtSignFailed"} and the SSE fallback takes over.
  8. Back up your `.env` file

    Copy the completed web/.env to 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-express
    git check-ignore -v web/.env
    # Expected: web/.gitignore prints, confirming the file is ignored.

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.

NameSourceRequiredNotes
POSTGRES_PASSWORDopenssl randYesEmbedded in DATABASE_URL as well
DATABASE_URLComposedYespostgresql://user:pass@host:5432/db
AUTH_SECRETopenssl randYes≥ 32 chars; rotating logs every user out
STEVE_API_KEYopenssl randYesMirrored into SteVe’s main.properties
CF_EMAIL_WORKER_SECRETopenssl randYesMirrored into Worker as POLARIS_SECRET_A
STEVE_PREAUTH_HMAC_KEYopenssl randYesMirrored into SteVe’s pre-auth webhook config
STEVE_METERVALUE_HMAC_KEYopenssl randYesMirrored into SteVe’s meter-value webhook config
LAGO_API_KEYLago dashboardYesFrom Lago → Developers → API keys
ADMIN_OIDC_CLIENT_SECRETPocket IDOptionalOnly if you’re using Pocket ID admin SSO
APNS_KEY_BASE64App Store ConnectOptionalOnly if shipping the iOS app

You can’t curl anything in Phase 1 — nothing’s running yet. Verify locally instead:

Terminal window
# Every required secret should be at least 32 characters.
cd web
grep -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:

  • .env does not appear in git status.
  • git check-ignore web/.env confirms it’s ignored.
  • Your secret-store entries match the values in .env exactly (whitespace and all — a trailing newline is a real bug here).

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.

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.