Skip to content

Production checklist

  1. Install and build on the server or in your build pipeline:

    Terminal window
    npm install
    npm run build # web app, docs and marketing site

    The API serves apps/web/dist (or WEB_DIST). The docs and marketing sites are static folders to host wherever you like.

  2. Apply migrations with the same environment as the API (see Database):

    Terminal window
    NODE_ENV=production npm run migrate -w @pdfeditor/api
  3. Start the API under a process manager of your choice, with NODE_ENV=production and the full configuration:

    Terminal window
    NODE_ENV=production npm start

    It stops gracefully on SIGTERM/SIGINT (finishes open requests, then closes database connections; it gives up after 10 seconds).

  4. Check GET /api/health. It returns {"ok":true} when the API can query the database.

  • NODE_ENV=production. This enforces the configuration check, turns off the development sign-in and /api/dev/*, and doesn’t auto-migrate unless MIGRATE_ON_START=true.
  • HTTPS on the public URL, and APP_URL set to exactly that URL. Teams only loads https:// apps, and the Entra redirect URIs must match it.
  • Reverse proxy in front of the API that terminates TLS and forwards to PORT. Set TRUST_PROXY to the number of proxies in front of the API (default 1), so client IPs in the activity log and the rate limits are right. Don’t set it higher than the real number of hops, or clients can spoof their IP with X-Forwarded-For. Allow request bodies of at least MAX_UPLOAD_MB through the proxy.
  • SESSION_SECRET: at least 32 random characters (openssl rand -hex 32), stored as a secret and never changed casually (see Configuration).
  • Microsoft Entra registration with production redirect URIs (guide), and a reminder to rotate the client secret before it expires.
  • Stripe live keys, prices created with stripe:setup, webhook endpoint with the right events, customer portal enabled (guide).
  • Mailgun domain verified and EMAIL_FROM on that domain (guide).
  • Audit log on insert-only and read-only roles, optionally in a separate database (guide).
  • Backups of the main database (it contains every document sent for signature) and the audit database, with tested restores.
  • First super admin created (guide).
  • Teams package built for the production URL (guide).
  • Monitoring: health check, and alerts on error-level logs, especially audit log write failed and background jobs failed.
  • Contact form: a Cloudflare Turnstile widget for the marketing hostname, TURNSTILE_SECRET_KEY and MARKETING_URL on the API, and PUBLIC_TURNSTILE_SITE_KEY in the marketing build.
  • Docs and marketing sites built with PUBLIC_SITE_URL, PUBLIC_APP_URL and the other PUBLIC_* values for production, and the marketing site serving /privacy and /terms (the Teams manifest links to them).

The API process runs a job 10 seconds after it starts and then every hour. It:

  • marks signing requests past their expiry date as Expired, and
  • sends automatic reminders that are due.

The API writes JSON logs (pino) to stdout, one line per request plus errors. Authorization and X-Entra-Token headers are redacted. Set the level with LOG_LEVEL. Unexpected errors return Something went wrong on our side to the client with code server_error, and the details go to the log.