HelloJohn / docs
Self-Hosting

Production Checklist

A complete checklist to verify before going live with a self-hosted HelloJohn instance.

Production Checklist

Review every item before sending real users to your HelloJohn instance.

Infrastructure

  • PostgreSQL is version 14 or later
  • Postgres runs on a separate host from HelloJohn (not in the same container without persistent volumes)
  • Database has automated backups — daily snapshots or continuous WAL archiving
  • Redis is configured for rate limiting and session caching (recommended for production)
  • Persistent volumes are mounted for any local storage (avatars, etc.)
  • Reverse proxy (nginx/Caddy/Traefik) handles TLS termination
  • TLS certificate is valid and auto-renewing (Let's Encrypt / ACM)
  • Port 3000 is not publicly exposed — only the reverse proxy port (443) is open
  • Firewall rules restrict access to PostgreSQL and Redis from HelloJohn only

Security

  • JWT_SECRET is at least 32 random bytes — generated with openssl rand -hex 32
  • Ed25519 key pair used instead of HMAC secret (preferred)
  • Secrets are not in version control — use .env files, Docker secrets, or a secrets manager
  • SESSION_COOKIE_SECURE=true is set (requires HTTPS)
  • CORS_ALLOWED_ORIGINS is set to your actual domains, not *
  • TRUST_PROXY=true is set with the correct TRUST_PROXY_HOPS value
  • Security headers are set by the reverse proxy (HSTS, X-Frame-Options, etc.)
  • HTTPS is enforced — HTTP redirects to HTTPS
  • Rate limiting is enabled (RATE_LIMIT_ENABLED=true)
  • Admin dashboard path is non-default or dashboard is disabled if not needed

Email

  • SMTP credentials are configured and tested
  • SPF record is set for your sending domain
  • DKIM is configured for your sending domain
  • DMARC policy is set to quarantine or reject
  • Magic link TTL is appropriate (default 15 minutes)
  • Test email delivery — trigger a password reset and verify it arrives

Application

  • APP_URL matches the exact public URL (no trailing slash, correct protocol)
  • ENV=production is set
  • LOG_LEVEL=info or warn (not debug in production — too verbose)
  • Health endpoint returns 200: curl https://auth.yourdomain.com/health
  • First admin account has been created and login has been tested
  • Admin password has been changed from the seeded default
  • OAuth redirect URIs are configured in each OAuth provider's dashboard

Observability

  • Logs are being collected — forwarded to a log aggregator (Datadog, Loki, CloudWatch, etc.)
  • Alerts are configured for error rate spikes and latency increases
  • Database metrics are monitored (connection count, query latency, cache hit rate)
  • Disk space is monitored — PostgreSQL WAL can fill disks quickly
  • Uptime monitoring is configured — e.g., Better Uptime, Checkly, or AWS Route 53 health checks

Multi-tenancy

  • Default tenant has been created (if applicable)
  • Per-tenant OAuth apps are configured (if different OAuth apps per tenant)
  • Email domain restrictions are set for tenants that require it

MFA

  • TOTP issuer name (MFA_TOTP_ISSUER) matches your product name
  • WebAuthn RP ID and origin are set to production values (not localhost)
  • Backup codes are enabled for users who enroll MFA

Disaster recovery

  • Backup restore has been tested — not just created, but actually restored
  • Recovery time objective (RTO) is documented and achievable with current backup strategy
  • Runbook exists for common failure scenarios (DB failover, key rotation, etc.)

Final checks

# Health check
curl https://auth.yourdomain.com/health

# TLS certificate validity
echo | openssl s_client -connect auth.yourdomain.com:443 2>/dev/null | \
  openssl x509 -noout -dates

# Security headers
curl -sI https://auth.yourdomain.com | grep -E \
  'Strict-Transport-Security|X-Frame-Options|X-Content-Type-Options|Referrer-Policy'

# Verify JWT signing works (sign in with a test user)
curl -X POST https://auth.yourdomain.com/v1/auth/sign-in \
  -H "Content-Type: application/json" \
  -d '{"email":"test@yourdomain.com","password":"testpassword"}'

Common mistakes

MistakeImpactFix
Weak JWT_SECRETToken forgery riskUse openssl rand -hex 32
APP_URL set to localhost in productionBroken magic links and OAuth redirectsSet to public HTTPS URL
No database backupsData loss on failureEnable automated backups
Port 3000 exposed publiclyBypasses TLS and proxy securityBind to 127.0.0.1:3000
LOG_LEVEL=debug in productionSensitive data in logsUse info or warn
CORS_ALLOWED_ORIGINS=*CSRF riskSet to your actual origins
Shared JWT secret across environmentsProd tokens valid in devUse separate secrets per environment

On this page