Skip to content

Configuration

Both services read every setting from environment variables, once, at start. There is no config file and there are no command-line flags; the only files are the keys some variables point to. A missing or invalid variable stops the service before it serves anything, and the error lists every problem at once, not just the first.

The full list, with defaults, is in Environment variables.

Shared settings are defined once in the platform with short names, and each service adds a prefix when it uses them. The services’ own settings have no prefix. The HTTP server’s ADDR becomes HTTP_ADDR; the MySQL PASSWORD becomes MYSQL_PASSWORD; the token verifier’s JWKS_URL becomes AUTH_JWKS_URL in the user service.

Every field carries a description, and the services can print their own reference:

Terminal window
go run ./cmd/user env

The website’s environment-variable page is generated from that output, so the docs can’t drift from the code.

Secrets, such as MYSQL_PASSWORD or SMTP_PASSWORD, use a Secret type. It prints [redacted] in logs, in JSON and with fmt, so a config struct can be logged safely. Code that needs the value calls Reveal().

Locally, compose.yaml sets development values inline, such as the dev-only-* database passwords, so there is no file to copy. In Kubernetes, secrets come from Secrets you create and name in auth.existingSecret and user.existingSecret; they are never written into Helm values.

APP_ENV is production by default. Set it to development only on your machine: it lets auth start without a signing key by generating a throwaway one, with a warning in the log. In any other environment a missing key stops auth at start.

auth signs access tokens with an ES256 key in PKCS#8 PEM format. Its kid is the key’s RFC 7638 thumbprint, so it never needs to be configured. Generate one:

Terminal window
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out signing-key.pem

Point SIGNING_KEY_FILE at it. In a cluster, mount it from a Secret.

auth also publishes every *.pem in PREVIOUS_KEYS_DIR, in name order, in its JWKS. Each must be a P-256 public key in PKIX PEM format (-----BEGIN PUBLIC KEY-----). A file that holds the signing key’s own public half is published once.

The other services keep the key set they fetched for up to an hour, so a token signed with a key they haven’t fetched yet can be refused. Publish a new key before signing with it:

  1. Make the new key and put its public half in previous-keys/, on every auth replica, then restart them. The JWKS now lists it.

    Terminal window
    openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out new-signing-key.pem
    openssl pkey -in new-signing-key.pem -pubout -out previous-keys/new.pem
  2. Wait more than an hour, so every service has fetched it.

  3. Put the old key’s public half in previous-keys/, point SIGNING_KEY_FILE at the new key, and restart auth. new.pem can stay.

    Terminal window
    openssl pkey -in signing-key.pem -pubout -out previous-keys/old.pem
  4. Tokens signed with the old key keep verifying until they expire, 15 minutes later. After another hour, remove old.pem.

Deploy to Kubernetes does the same with the chart’s Secrets.

The store is chosen when the binary is built, not when it starts: a build tag links exactly one backend.

Store Build tag Docker build arg
Memory none STORE=memory (the default)
MySQL persistence_mysql STORE=mysql
MongoDB persistence_mongodb STORE=mongodb

MySQL needs its schema applied. Run <service> migrate before starting the new version, which is what the Helm chart’s migration hook does, or set MIGRATE_ON_START=true to migrate at start, which compose does. MongoDB has no schema to migrate: the same migrate command, or MIGRATE_ON_START, ensures its indexes, and running it again changes nothing.

MongoDB must run as a replica set, even a single node, because the services write events, projections and the outbox in one multi-document transaction.

The rate limiter needs the client’s address. By default a service uses the TCP peer. Behind a load balancer, that is the load balancer, so set TRUSTED_PROXIES to its CIDRs, comma-separated. The service then takes the right-most X-Forwarded-For hop that isn’t a trusted proxy. A client can prepend anything to that header, so only hops your own proxies added are believed.

The limiter is a token bucket per client: RATE_LIMIT_RATE requests per second, bursts up to RATE_LIMIT_BURST, and at most RATE_LIMIT_VISITORS clients tracked.

The user service sends mail through SMTP with SMTP_ADDR, SMTP_USERNAME, SMTP_PASSWORD and SMTP_FROM. It refuses a server that doesn’t offer STARTTLS. SMTP_ALLOW_INSECURE=true allows one, for Mailpit on your machine only, and even then the password is sent in clear text only to localhost.

WEB_URL is the web app’s public address. Links in emails point there.

Google sign-in is off until GOOGLE_CLIENT_ID is set. To turn it on:

  1. Create an OAuth client of type Web application in the Google Cloud console.
  2. Add the callback as an authorized redirect URI: your public address followed by /api/user/v1/google/callback, such as http://localhost:8080/api/user/v1/google/callback locally.
  3. Set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET and GOOGLE_REDIRECT_URL (the same callback URL).

The web app shows the Google button once the user service reports it enabled.