# Self-hosting

Checkpulse runs as two services on one PostgreSQL database:

| Service | Command | Purpose |
|---|---|---|
| Application | `checkpulse` | Dashboard, pings, API, MCP, deadlines, and alert delivery. Runs migrations at startup. Must run continuously. |
| Admin console | `checkpulse-admin` | Accounts, sign-up, limits, and invitations. Connects as the restricted `checkpulse_admin` role. Can sleep when idle. |

Both are in the same Docker image; the admin console only needs a different start command.

## Database roles

The application's database user creates the `checkpulse_admin` role during migrations, so it needs `CREATEROLE` (a superuser, such as the default user of a managed PostgreSQL service, works). The role can read account details and aggregate counts, and change account status, limits, and settings. It has no privileges on projects, checks, pings, channels, alerts, or API keys, so the admin console cannot show them even if it is compromised. People with direct database access can still read everything.

Set `ADMIN_DATABASE_PASSWORD` on the application to let the role log in, then connect the admin console with that password.

## Application settings

| Variable | Purpose |
|---|---|
| `DATABASE_URL` | PostgreSQL connection URL. Required. |
| `PUBLIC_URL` | The HTTPS origin users visit, such as `https://checkpulse.example`. |
| `ADMIN_DATABASE_PASSWORD` | Password for the `checkpulse_admin` role. |
| `CONTACT_EMAIL` | Shown on the terms and privacy pages. |
| `EMAIL_PROVIDER` | `resend`, `postmark`, or `smtp`. Sign-up, password reset, and email alerts need it. |
| `EMAIL_FROM` | Sender address on a domain verified with the provider. |
| `RESEND_API_KEY`, `POSTMARK_SERVER_TOKEN` | Provider credentials. |
| `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_MODE` | SMTP settings. Port 587 uses STARTTLS; `SMTP_MODE=tls` uses implicit TLS; `plain` is allowed only for local development. |
| `TRUSTED_PROXY_HEADER` | Header with the client address. Defaults to `X-Real-IP` on Railway; set `none` when the app is reached directly. |
| `ALLOW_PRIVATE_WEBHOOKS` | `true` allows HTTP and private-network destinations. Local development only. |

## Admin console settings

| Variable | Purpose |
|---|---|
| `DATABASE_URL`, or `PGHOST`, `PGPORT`, `PGDATABASE`, `PGUSER=checkpulse_admin`, `PGPASSWORD` | Connection as the restricted role. |
| `PUBLIC_URL` | The console's own HTTPS origin. |
| `APP_URL` | The application's origin, used in invitation links. |
| `ADMIN_EMAIL`, `ADMIN_PASSWORD` | Create the first operator on first start. They can be removed afterwards. |

Operators must set up two-factor sign-in the first time they sign in.

## Local development

```sh
cp .env.example .env   # choose ADMIN_PASSWORD
docker compose up --build -d
```

The app is at http://localhost:8080, the admin console at http://localhost:8081, and the Mailpit inbox (all email) at http://localhost:8025. Sign in to the admin console, then create an invitation or open sign-up to make your first account.

Without the admin console, `checkpulse invite you@example.com` prints a single-use sign-up link.

## Email

Railway disables outbound SMTP below its Pro plan, so hosted deployments should use an email API. Resend's free tier (3,000 emails a month, 100 a day) is enough to start; Postmark costs about $15 a month for 10,000 and has a strong reputation for transactional mail. Either needs a domain you own, verified with SPF and DKIM.

## Operations

- **Backups:** enable your database provider's backups before inviting users.
- **Monitor the monitor:** Checkpulse cannot alert about its own downtime. Point an external heartbeat service at a check that a separate machine pings, or watch `/healthz` from outside.
- **Retention:** history is trimmed hourly to each account's limit; delivered notifications are kept 30 days; audit logs and closed incidents 400 days; deleted checks 7 days.
- **Upgrades:** migrations run automatically when the application starts, inside a transaction guarded by an advisory lock. The admin console waits for them.
