Secrets Management

Where secrets live, how they're loaded, and how to rotate them without downtime.

This section is intended for: Technical Team. Unauthorised access is restricted.

Audit status: MixedEnv hierarchy + boot validation: Implemented. Rotation procedure: Partial — documented, not automated.

Where secrets live, how they're loaded, and how to rotate them without downtime.

Status: Implemented (env hierarchy + boot-time validation) + Partial (rotation procedure — documented, not automated).

Env-var hierarchy

Govula reads ALL configuration from environment variables. There is no on-disk config file. The hierarchy is, in priority order:

  1. Process env at runtime (set by Railway / Vercel / your platform / docker run -e).
  2. .env file at repository root (development only; ignored by .gitignore).
  3. Hard-coded defaults in src/config/index.ts and src/config/communications.ts — only safe-by-design defaults (HOST=0.0.0.0, EMAIL_FROM=…@govula.com).

Boot validation in src/config/validateEnv.ts refuses to start in production when any required secret is missing or weak. The full template is .env.example.

Required vs optional, at a glance

CategoryVariablesBehaviour when unset
Required in prodDATABASE_URL, JWT_SECRET (≥32 chars), JWT_REFRESH_SECRET (≥32 chars), CORS_ALLOWED_ORIGINS (no *), PUBLIC_URL, RESEND_API_KEY + ALERT_EMAIL_TO + EMAIL_FROM (all-or-none)Boot fails with a structured error
RecommendedSENTRY_DSN, RAILWAY_API_TOKEN, VERCEL_API_TOKEN, NEON_API_KEY, every COMMS_*Boot succeeds, structured warn emitted
OptionalFeature flags, cron toggles, federation keys, SSO configFeature degrades or stays off

Per-platform secret stores

Railway (canonical backend host)

  • Project → service → Variables tab.
  • Secrets are encrypted at rest, decrypted into the container env at boot.
  • Changes trigger a redeploy.

Vercel (canonical frontend host)

  • Project → Settings → Environment Variables.
  • Mark each variable with the appropriate scope (Production / Preview / Development).
  • NEXT_PUBLIC_* vars are baked into the client bundle at build time — changing them requires a rebuild, not just a redeploy.

Self-hosted Docker

Three options, in increasing rigour:

MethodWhenNotes
docker run -e KEY=valueLocal devVisible in docker inspect
--env-file .envSingle-host prodFile must be chmod 600, owned by root
Docker secrets (/run/secrets/<name>)Swarm or Compose v3.1+Requires app to read from file path; Govula does NOT support this out of the box

For Docker secrets support, wrap the container entrypoint:

#!/bin/sh
export JWT_SECRET="$(cat /run/secrets/jwt_secret)"
exec node dist/index.js

Self-hosted with sops / Vault

  • sops + age/PGP: encrypt .env at rest in git; decrypt on the deploy host into ephemeral env. Works well with GitOps.
  • HashiCorp Vault: use the Vault Agent sidecar to template env vars before app start. Govula reads its env normally; Vault is invisible to the app.

The communication identity registry (T21)

src/config/communications.ts is the single source of truth for every external-facing email identity (support, security, governance, investor, founder-alerts, notifications). Each reads a COMMS_* env var with a clearly non-production @govula.com placeholder fallback. See docs/communications.md for the full table.

Implication for rotation: changing a contact address is a one-env-var change with no code deploy.

Federation keypair

GPS uses Ed25519 signatures. The keypair lives in FEDERATION_PRIVATE_KEY / FEDERATION_PUBLIC_KEY. Generate with the helper in src/services/federationCryptoService.ts.

Rotation:

  1. Generate new keypair.
  2. Set new FEDERATION_PUBLIC_KEY on every peer first (peers verify with this).
  3. Wait until peers have rotated.
  4. Set new FEDERATION_PRIVATE_KEY on this instance and restart.
  5. Old public key can be removed after one signal cycle.

JWT secret rotation

JWT_SECRET and JWT_REFRESH_SECRET are rotated independently:

  • JWT_SECRET signs access tokens (default 1h TTL). Rotating logs everyone out at most 1h after the rotation. To rotate without downtime, accept BOTH old and new during a transition window — Govula does not currently ship dual-secret support, so the operational reality is "rotate during a low-traffic window, accept brief reauth".
  • JWT_REFRESH_SECRET signs refresh tokens. Rotation immediately invalidates every refresh token (forcing reauth on next refresh). This is exactly the documented hazard of leaving it unset (see ../audits/phase1-readiness-audit.md F-A3).

Rotation policy (recommended cadence)

SecretCadenceProcedure
JWT_SECRETAnnual or on suspected compromiseSet new value; restart; users reauth on next request
JWT_REFRESH_SECRETAnnual or on suspected compromiseSame; users reauth on next refresh
RESEND_API_KEYAnnualGenerate new key in Resend; swap env var; redeploy
FEDERATION_PRIVATE_KEYQuarterlyTwo-phase rollout above
DATABASE_URL (creds)When DBA rotatesUpdate env, redeploy; pool drains and reconnects
Observability tokensWhen platform rotatesEach is independently optional; degrades gracefully

What NOT to do

  • Do not put secrets in replit.md, README.md, or any doc.
  • Do not put secrets in build args without --secret mounts.
  • Do not commit .env (already in .gitignore).
  • Do not print secrets in logs — Pino redaction (src/utils/logger.ts:55-63) covers password, token, secret, authorization, cookie, apiKey at top-level, nested, and metadata.* paths, but new keys you add are not auto-redacted.

Where to read more

Canonical source: docs/deployment/secrets-management.md

This page mirrors the markdown deployment hub on disk. The full markdown source includes additional code blocks, command examples, and embedded reference tables.

Hub index: /docs/deployment

You are here · Deploy · step 13
Observability Setupnext step

Next in Deploy: Observability Setup.

What should I do next?

Activation Flowprimary

continues in "deploy"

Ranked using IA v1 graph + intent map + glossary density (deterministic; no AI inference).