Configuration
Selvara is configured in two places. Deployment wiring — where the database is, what signs the sessions, how long metrics are kept — is environment. Everything an administrator changes while the instance is running — the name on the sidebar, the mail server, what agents are told to do — lives in the database and is edited in the dashboard.
This page lists every environment variable the code actually reads. It was
built by reading the source, not the compose file, so it includes a few the
shipped docker-compose.yml does not set.
Application
These are read by the running dashboard.
| Variable | Purpose | Required | Default | If wrong or missing |
|---|---|---|---|---|
DATABASE_URL | PostgreSQL connection string for the app and the migrations. | Yes | none | scripts/migrate.mjs prints DATABASE_URL is not set and exits 1, so the container never starts. A wrong host or password produces connection errors on every request and "database":"error" from /api/health. |
DATABASE_POOL_MAX | Maximum node-pg pool connections. | No | 20 | Too low starves requests under load; too high exhausts max_connections on the server (the compose file sets it to 200). Note that pool options in the connection string are ignored — this variable is the only knob. |
DATABASE_STATEMENT_TIMEOUT_MS | Per-statement timeout for the app's pool. | No | 30000 | Raising it lets one runaway history query pin a connection for minutes. Lowering it makes slow dashboards fail instead of crawl. |
REDIS_URL | Redis for the live updates and rate-limit counters. | In practice yes | redis://localhost:6379 | Unreachable Redis does not take the app down: rate limiting fails open and logs, and /api/health reports "redis":"error" with HTTP 503. Open dashboards stop updating until it is back. |
AUTH_SECRET | Signs NextAuth session cookies. Also the fallback key for encrypting stored secrets — see below. | Yes | none | Compose refuses to start without it. Changing it invalidates every session, and without SETTINGS_ENCRYPTION_KEY it also destroys access to stored secrets. |
AUTH_URL | The public base URL. NextAuth derives its callback and CSRF endpoints from it; notification and e-mail links are built from it. | Yes | falls back to an empty string in link-building code | A mismatch with the address in the browser breaks sign-in. An empty value produces notification links that go nowhere. No trailing slash. |
NEXTAUTH_URL | The same public base URL. /api/agent/install.sh and /api/agent/version read only this one, with no fallback to AUTH_URL. | Yes | none | With it unset, the generated agent install command and the agent download URL come out with an empty host, so agents cannot update themselves. Always set it to the same value as AUTH_URL. |
WEBHOOK_SECRET | Shared HMAC secret. Agents sign a short-lived JWT with it; the dashboard verifies every agent request against it, and writes it into the config.yaml the installer fetches. | Yes | none | Compose refuses to start without it. A mismatch makes every agent request 401 and the host looks offline. /api/agent/config answers 500 Server misconfigured when it is unset. |
SETTINGS_ENCRYPTION_KEY | Derives the AES-256-GCM key for secrets stored in the database. | No, but set it | falls back to AUTH_SECRET | See the section below. This one is a foot-gun. |
CRON_SECRET | Bearer token the alert-evaluation endpoint checks. | No in code, yes in the shipped compose file | none | See the section below. Unset means the endpoint is open. |
NODE_ENV | Standard Node environment. The image sets production. | Set by the image | production in the image | Outside production the trusted-device cookie is not marked Secure. Do not override it in a deployment. |
PORT | Port the server binds. | Set by the image | 3000 | Change it and the compose healthcheck and port mapping, which both name 3000, stop matching. |
HOSTNAME | Bind address. | Set by the image | 0.0.0.0 | Binding to loopback inside the container makes it unreachable from the proxy and from the other containers. |
NEXT_TELEMETRY_DISABLED | Turns off Next.js telemetry. | Set by the image | 1 | No operational effect. |
Retention and TimescaleDB
These are read once per container start, by scripts/migrate.mjs, when it
configures the hypertable and its policies. They are not read by the running
app.
| Variable | Purpose | Default |
|---|---|---|
METRICS_CHUNK_INTERVAL | Hypertable chunk width for metrics. Retention drops whole chunks, so this is also the granularity at which space comes back. | 1 day |
METRICS_RAW_RETENTION | How long full-resolution samples are kept. | 7 days |
METRICS_COMPRESS_AFTER | Age at which raw chunks are compressed. Typically 10–20x on this kind of data. | 2 days |
METRICS_5M_RETENTION | How long the metrics_5m rollup is kept. Also bounds the one-time backfill when that rollup is first created. | 30 days |
METRICS_1H_RETENTION | How long the metrics_1h rollup is kept. This is the longest history any chart can show. | 400 days |
METRICS_MAX_MIGRATE_BYTES | Safety rail. If metrics is still a plain table and is larger than this, the conversion to a hypertable is refused rather than attempted, because converting rewrites the whole table and needs that much free disk again. | 21474836480 (20 GiB) |
All of these accept a PostgreSQL interval string, so 12 hours, 14 days and
6 months are all valid.
Changing one and restarting is not always enough. The policies are registered
with if_not_exists => true, which leaves an existing policy alone rather than
rewriting its interval. Check what is actually installed:
docker exec -i selvara-db psql -U monitoring -d selvara -c \
"SELECT job_id, application_name, config FROM timescaledb_information.jobs
WHERE application_name NOT LIKE 'Telemetry%';"
If a policy still shows the old interval, drop it and let the next start re-register it:
docker exec -i selvara-db psql -U monitoring -d selvara -c \
"SELECT remove_retention_policy('metrics');"
docker compose restart app
Shortening retention deletes data on the policy's next run and that data is gone. Take a dump first — Backup and Restore.
Compose-level
| Variable | Purpose | Default |
|---|---|---|
POSTGRES_PASSWORD | Interpolated into both the postgres service's POSTGRES_PASSWORD and the app's DATABASE_URL. | monitoring_secret — change it |
PostgreSQL only applies this when it initialises an empty data directory.
Changing it later changes the password the app connects with but not the one
the server expects, and the app then cannot reach the database. To rotate it,
change it inside PostgreSQL with ALTER ROLE and update .env to match.
CRON_SECRET is optional, and that has consequences
The alert-evaluation endpoint checks the bearer token only if the variable is set:
const cronSecret = process.env.CRON_SECRET;
if (cronSecret && authHeader !== `Bearer ${cronSecret}`) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
With CRON_SECRET unset, POST /api/cron/evaluate-alerts — and GET, which
is wired to the same handler — is unauthenticated. Anyone who can reach the
dashboard can run an alert evaluation cycle, which writes alerts, marks systems
offline and sends notifications.
The shipped compose file marks it required for both the app and the cron
container, so a Compose deployment cannot start without one. If you run the app
some other way, set it. The cron container must be given the same value: it
writes the token into a small script rather than into the crontab line, which
keeps the secret out of /etc/crontabs and out of the process list.
SETTINGS_ENCRYPTION_KEY falls back to AUTH_SECRET
Secrets in the database are encrypted with AES-256-GCM using a key derived from
SETTINGS_ENCRYPTION_KEY — or, when that is unset, from AUTH_SECRET:
const secret = process.env.SETTINGS_ENCRYPTION_KEY || process.env.AUTH_SECRET;
What that key protects:
- the SMTP password (
app_settings) - every agent setting marked as a secret, such as a CrowdSec bouncer key
(
agent_settings) - every user's TOTP secret (
users.totp_secret)
None of this fails loudly. Decryption failures are caught and logged, and the
caller gets null — so mail silently stops sending, agents silently lose a
credential, and users with two-factor authentication enabled can no longer sign
in because the server cannot recover the secret to check their codes against.
The trap: if you never set SETTINGS_ENCRYPTION_KEY and later rotate
AUTH_SECRET, all of that becomes undecryptable at once. Rotating a session
secret is an ordinary thing to want to do, and there is nothing in the act of
doing it that suggests it will take the mail server with it.
Set SETTINGS_ENCRYPTION_KEY to its own random value before you store
anything, and treat it as permanent. Back it up with the database dump — a
dump without the key is a dump with holes in it. If the key is already gone,
the recovery is to re-enter the SMTP password and the agent secrets in the
dashboard and to have affected users re-enrol their second factor.
What is configured in the dashboard
These are not environment variables. They live in the database, are edited in the UI, and survive recreating the container.
| Setting | Where | Notes |
|---|---|---|
| SMTP server | Settings | Host, port, encryption (SSL/TLS, STARTTLS or none), user, password, sender address and name, enabled. The password is encrypted. Until SMTP is enabled, POST /api/auth/password/forgot answers 503 Mail is not configured and the login screen does not offer a reset link. |
| Agent settings | Agents, and per cluster or per system | Values the dashboard hands down to agents, layered cluster first and system on top. Secrets are encrypted at rest and are sent in the clear only to the agent they belong to. See Agent Settings. |
| Agent rollout mode | Agents | Whether updates go out to everything at once or one canary per customer first. See Agent Rollout. |
| Alert retention | Settings → Data retention | Days after which resolved alerts are deleted, checked once an hour. Default 90; 0 keeps them until someone deletes them. Open alerts are never deleted. Administrators only. |
| Alert rules, event rules, maintenance windows | Settings | See Alert Rules, Event Rules, Maintenance Windows. |
The split is not arbitrary. Environment holds what the process needs before it can talk to anything — where the database is, what signs a cookie. Everything else is instance data: an administrator changes it at runtime without shell access, it takes effect without a restart, and because it is in the database it is covered by your backup. That is why an administrator can change an alert rule without a redeploy, and why a restored dump comes back with the same SMTP settings it had.
The name and the mark are not in that table. They ship with the application and are the same on every instance; there is nothing to configure and nothing to restore.