Deployment, migrations & CI¶
Configuration¶
All configuration is done through environment variables. See .env.example for the full list.
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL |
yes | PostgreSQL connection string |
SESSION_SECRET |
yes | Signing key for session cookies and key for the address counters of the form rate limit (openssl rand -hex 32). Without it the web app does not start; below 32 characters it logs a warning. The worker does not need it. |
SETTINGS_ENCRYPTION_KEY |
yes for secrets | 64 hex characters, encrypts SMTP, Resend, Twilio, webhook and VAPID secrets |
SETTINGS_ENCRYPTION_KEY_OLD |
only when rotating | Previous key for pnpm db:rotate-secret-key; remove it afterwards |
ADMIN_PASSWORD_HASH |
optional | Bootstrap admin, only until a database admin exists |
ADMIN_EMAIL |
optional | E-mail of the bootstrap admin, default admin@status.local |
ADMIN_BOOTSTRAP_DISABLED_AFTER |
optional | ISO date after which the bootstrap login is refused |
APP_URL |
recommended | Public base URL for links in e-mails, double opt-in and the RSS feed (/feed.xml). The worker needs it as well. |
NEXT_PUBLIC_STATUS_PAGE_URL |
optional | Takes precedence over APP_URL for links. Order: NEXT_PUBLIC_STATUS_PAGE_URL, APP_URL, http://localhost:<PORT>. |
NEXT_PUBLIC_STATUS_PAGE_NAME |
optional | Fallback name while no site name is stored in the settings, and default for db:seed. The displayed name comes from the settings. |
TRUSTED_PROXY_IP_HEADER |
optional | Where the client IP comes from: auto (default), x-real-ip or x-forwarded-for |
TRUSTED_PROXY_COUNT |
optional | Number of trusted proxies in x-forwarded-for mode, default 1 |
AUTH_RATELIMIT_* |
optional | Login and API-token throttling. AUTH_RATELIMIT_ENABLED=false disables it. |
PUBLIC_FORM_RATELIMIT_* |
optional | Throttling of the public forms: _ENABLED, plus _IP_, _EMAIL_ and _UNKNOWN_IP_, each with MAX, WINDOW_MIN and LOCK_MIN. Defaults 10/60/60, 1/10/10 and 100/60/60. |
STATUS_PAGE_EMAIL_DRIVER |
optional | capture or log records mails instead of sending them and always reports success (tests) |
STATUS_PAGE_SMS_DRIVER |
optional | capture records SMS instead of sending them (tests) |
LOG_LEVEL, LOG_FORMAT |
optional | Logger threshold (debug–error) and output (json or pretty) |
NEXT_ALLOWED_DEV_ORIGINS |
development only | Additional hosts for the dev server, comma-separated |
Mail, SMS, webhook and push providers are configured in the admin UI, not in environment variables. The exception is SETTINGS_ENCRYPTION_KEY, which must be set in the environment.
Warning
.env.example sets STATUS_PAGE_EMAIL_DRIVER=capture. Remove that line if you use the file as a template for production. Otherwise every dispatch reports success without any mail going out.
Danger
The encryption key is not stored in the database. If it is lost, encrypted secrets cannot be read and must be entered again. Back it up together with the database dump.
Encrypting secrets and rotating the key¶
- If
SETTINGS_ENCRYPTION_KEYwas only set later,pnpm db:encrypt-secretsencrypts the secrets that were stored as plaintext so far, once. - To rotate, set the previous key as
SETTINGS_ENCRYPTION_KEY_OLDand the new one asSETTINGS_ENCRYPTION_KEY, runpnpm db:rotate-secret-key, then removeSETTINGS_ENCRYPTION_KEY_OLD. - Against a non-local database, both scripts only run with
--yes-i-am-sure.
Docker¶
| File | Purpose |
|---|---|
Dockerfile |
Web app image. Runs next start -p 3100. Contains a HEALTHCHECK on /api/health. |
Dockerfile.worker |
Worker image. Same code base, no build step, no port. Run it as a second application. |
docker-compose.yml |
Local PostgreSQL 16 (port 5433) and an optional E2E application container |
docker build -t slatus .
docker run -p 3100:3100 --env-file .env slatus
Info
The image listens on port 3100. Map this port, not 3000. If SESSION_SECRET is missing, the container does not start and the HEALTHCHECK fails.
Deploy the web app and the worker from the same repository, with identical DATABASE_URL and SETTINGS_ENCRYPTION_KEY. Run one worker instance.
Reverse proxy¶
The rate limits count per client IP. Run SLAtus behind a trusted reverse proxy with HTTPS that forwards the client IP, and set TRUSTED_PROXY_IP_HEADER to match:
- Proxy sets
X-Real-IP(for example nginx withproxy_set_header X-Real-IP $remote_addr;):TRUSTED_PROXY_IP_HEADER=x-real-ip. - Proxy only appends to
X-Forwarded-For:TRUSTED_PROXY_IP_HEADER=x-forwarded-for, andTRUSTED_PROXY_COUNTset to the number of proxies in front of the app. autoreadsx-real-ip, otherwise the lastx-forwarded-forentry. Behind a pureX-Forwarded-Forproxy, a client can then inject its ownX-Real-IPheader.- Without a proxy, Next.js only fills
X-Forwarded-Forif the header is missing. A client can then fake its IP. - If the proxy sets no IP header, all visitors share the proxy's IP. In production, the app logs this once as a warning.
Details on the detection are in Auth & security.
Migrations¶
- Migrations are forward-only. Each migration is an SQL file in
drizzle/, generated withpnpm db:generatefromlib/db/schema.ts. - They are applied only with
pnpm db:migrate(scripts/migrate.ts). Each migration runs in its own transaction, because PostgreSQL can use a new enum value only after commit. - Rollbacks are documented, not automated. See
docs/context/migrations.mdin the repository.
Typical deployment order: stop the worker and web app (or deploy with a short pause), run pnpm db:migrate, start the new version. The worker creates missing template rows for new event types when it starts.
Local development¶
cp .env.example .env # set SESSION_SECRET, SETTINGS_ENCRYPTION_KEY, ADMIN_PASSWORD_HASH
pnpm install
pnpm db:up # PostgreSQL in Docker on port 5433
pnpm db:migrate
pnpm db:seed # demo data; dev admin: admin@status.local / admin123
pnpm dev # http://localhost:3000
pnpm worker # second terminal, required for notifications and checks
The seed scripts delete data. They refuse to run against anything other than a local host or a database whose name ends in _dev, _e2e or _test, unless --yes-i-am-sure is passed. All db:* scripts read .env.
With Next.js 16, pnpm dev writes to .next/dev and pnpm build to .next, so both can run in parallel. A second next dev in the same directory is rejected. The E2E tests use a separate directory .next-e2e through NEXT_DIST_DIR.
Quality gates¶
| Command | What it checks |
|---|---|
pnpm typecheck |
TypeScript (tsc --noEmit) |
pnpm lint |
ESLint |
pnpm test |
Unit tests (Vitest) |
pnpm test:integration |
Integration tests against a real database |
pnpm build |
Production build |
./scripts/agent-verify.sh |
Typecheck, lint, unit tests, db:migrate (only if the database is reachable on localhost:5433) and build. No integration tests. E2E only with RUN_E2E=1. |
pnpm test:e2e |
Playwright end-to-end tests with a dedicated statuspage_e2e database. Two projects: main with all specs, then i18n-de for the German UI. The default is next dev; the e2e-app container is only used with E2E_USE_DOCKER=1. |
Continuous integration¶
.github/workflows/ci.yml runs on pushes to main, on release-* tags and on pull requests. A new push cancels the running job of the same branch. Documentation-only changes (docs/**, .claude/**, **/*.md) do not trigger CI.
The workflow starts a PostgreSQL 16 service and runs, in order:
pnpm audit --audit-level high(blocking). Known exceptions are listed inpnpm-workspace.yamlunderauditConfig.ignoreGhsas, security overrides underoverrides.pnpm db:migrateandpnpm db:seed./scripts/agent-verify.shpnpm test:integration(blocking)- the Playwright suite
Mirror: .github/workflows/mirror.yml publishes the deployable application code, without the agent setup and documentation, to the deploy repository Mach-IIDeV/slatus-web on release-* tags. The files to mirror are listed in scripts/app-files.txt. The workflow can also be started manually with any commit, tag or branch.