This document describes how secrets are stored, validated, and rotated in the MyFans backend.
| Variable | Purpose | Required | Rotation frequency |
|---|---|---|---|
JWT_SECRET |
Signs and verifies JWT access tokens | Yes | On compromise; recommended every 90 days |
JWT_ACCESS_EXPIRES_IN |
Access token TTL in seconds (default 900) | No | When session policy changes |
DB_PASSWORD |
PostgreSQL authentication | Yes | On compromise; recommended every 90 days |
WEBHOOK_SECRET |
HMAC-SHA256 signing of outbound webhooks | Yes | On compromise; recommended every 30 days |
DB_HOST, DB_PORT, DB_USER, DB_NAME |
Database connection | Yes | When infrastructure changes |
SOROBAN_RPC_URL |
Soroban RPC endpoint | Yes | When provider changes |
All required variables are validated at startup via src/common/secrets-validation.ts. The app exits immediately if any are missing.
- Never commit
.envor any file containing real secret values to version control. - Use
.env.exampleas the template; copy it to.envlocally and fill in values. - In production, inject secrets via your platform's secret manager (e.g. AWS Secrets Manager, HashiCorp Vault, GitHub Actions secrets) as environment variables.
- Restrict read access to
.envfiles:chmod 600 .env.
Rotating JWT_SECRET immediately invalidates all existing sessions. Plan for a brief re-login window.
- Generate a new secret:
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))" - Update
JWT_SECRETin your secret manager / deployment environment. - Redeploy the backend. All existing JWTs are immediately invalid.
- Notify users that they will need to log in again.
Run two backend instances briefly in parallel — old instance keeps the old secret, new instance uses the new secret — then drain the old instance once access tokens expire (JWT_ACCESS_EXPIRES_IN, default 900 s).
- Deploy new instance with the new
JWT_SECRET. - Keep old instance running until in-flight tokens expire (≤ 15 min with default TTL).
- Decommission old instance.
- Refresh tokens issued before rotation will fail on the new instance; users will be prompted to re-authenticate.
Store JWT_SECRET as a GitHub Actions secret and reference it in your workflow:
env:
JWT_SECRET: ${{ secrets.JWT_SECRET }}To rotate in CI:
- Go to Settings → Secrets and variables → Actions.
- Update
JWT_SECRETwith the new value. - Re-run or trigger a new deployment workflow.
# Confirm startup probe passes with new secret
curl -sf http://localhost:3000/v1/health | jq .status
# Attempt login and verify a new JWT is issued
curl -s -X POST http://localhost:3000/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"publicKey":"<G-address>","signature":"<sig>"}' | jq .accessToken- Create the new password in your database:
ALTER USER myfans WITH PASSWORD '<new-password>';
- Update
DB_PASSWORDin your secret manager. - Redeploy the backend (or restart the process) to pick up the new value.
- Verify connectivity:
npm run start:prodshould pass the DB startup probe. - Remove the old password from any local notes or CI variables.
The backend supports zero-downtime webhook secret rotation via a grace-period window. The previous secret remains valid for up to 24 hours after rotation so in-flight webhook deliveries are not rejected.
Using the CLI script:
# 1. Rotate to a new secret (previous secret valid for 24 h by default)
API_BASE_URL=https://api.myfans.example.com \
ts-node scripts/rotate-webhook-secret.ts rotate <new-secret>
# 2. (Optional) Shorten the grace period to 1 hour
API_BASE_URL=https://api.myfans.example.com \
ts-node scripts/rotate-webhook-secret.ts rotate <new-secret> 3600000
# 3. Once all clients have updated their signing key, expire the previous secret immediately
API_BASE_URL=https://api.myfans.example.com \
ts-node scripts/rotate-webhook-secret.ts expire-previousManual steps:
- Generate a new secret:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" - Call
POST /v1/webhook/rotatewith{ "newSecret": "<new-secret>" }. - Update
WEBHOOK_SECRETin your secret manager and redeploy so the new value is used on the next restart. - Notify webhook consumers to update their signing key.
- After the grace period (or once all consumers have updated), call
POST /v1/webhook/expire-previous.
Verify a signature locally:
ts-node scripts/rotate-webhook-secret.ts sign <secret> <payload>src/common/secrets-validation.ts checks that all required secrets are non-empty before the NestJS application finishes bootstrapping. If any are missing the process exits with a clear error listing every missing variable:
[secrets-validation] Missing required environment variables:
- JWT_SECRET
- DB_PASSWORD
See backend/.env.example for the full list of required variables.
This prevents the app from starting in a partially-configured state that could silently fall back to insecure defaults.
Use this checklist after any secret rotation to confirm the change is complete:
- New secret generated with sufficient entropy (≥ 32 random bytes).
- Secret updated in the secret manager / deployment environment.
- Backend redeployed (or process restarted) and startup probes pass.
- Old secret removed from all local notes, CI variables, and chat logs.
- For
WEBHOOK_SECRET: webhook consumers notified and previous secret expired after grace period. - For
JWT_SECRET: users re-authenticated or re-login window communicated. - No plaintext secret values appear in application logs (see
docs/log redaction guidance).