Configuration
Everything is configured through environment variables
(.env next to the app; in Docker via
env_file). There is no config UI beyond the board itself —
this is deliberate: config-as-file is easy to back up, diff, and
revert.
Copy .env.example to .env and edit. Only
SESSION_SECRET is required; everything else is optional and
the board degrades gracefully.
Core
| Variable | Default | Purpose |
|---|---|---|
SESSION_SECRET |
— | Required. Long random string; signs session
cookies. Generate:
-join ((48..57)+(65..90)+(97..122) | Get-Random -Count 48 | % {[char]$_}) |
PORT |
3010 |
HTTP port |
DATA_DIR |
./data |
All runtime state (db.json, backups/, license key, trial state).
Docker sets this to /app/data. |
PUBLIC_BOARD_URL |
(empty) | Public base URL used in emailed links (e.g.
https://status.example.com) |
Identity
| Variable | Default | Purpose |
|---|---|---|
ORG_NAME |
(empty) | Display name for your organization — replaces "Operational Transparency Board" in the browser title, header, and wall dashboard. Empty = product default. |
EMAIL_DOMAIN |
(empty) | Restrict self-service subscriptions to one email domain (e.g.
example.com). Empty = anyone may subscribe. |
Email is how staff find out an incident is happening. The board works
without it, but sends nothing — so configure it before going live.
For the full walkthrough — per-provider settings, the Microsoft
365 and Google traps, DNS records and a troubleshooting table — see
INSTALL.md, "Setting up email".
Leave SMTP_HOST empty to disable email entirely. When
email is off, the subscribe form says so honestly rather than silently
swallowing subscriptions.
| Variable | Default | Purpose |
|---|---|---|
SMTP_HOST |
(empty) | SMTP relay hostname. Empty = email disabled. |
SMTP_PORT |
587 |
587 = STARTTLS, 465 = implicit TLS |
SMTP_USER / SMTP_PASS |
(empty) | SMTP credentials (omit for unauthenticated relays) |
SMTP_SECURE |
false |
Implicit TLS. Must match the port —
false for 587, true for 465 |
SMTP_EHLO_NAME |
(empty) | HELO/EHLO name presented to the relay. Rarely needed. |
SMTP_FROM |
(empty) | From address. Empty = ORG_NAME <SMTP_USER> |
OPERATIONS_DL |
(empty) | Distribution list copied on every outage notification |
Quick reference for common providers
| Provider | SMTP_HOST |
SMTP_USER |
SMTP_PASS |
|---|---|---|---|
| Google Workspace | smtp.gmail.com |
full address | app password (needs 2FA) |
| Microsoft 365 | smtp.office365.com |
full address | account password — SMTP AUTH must be enabled on the mailbox |
| Amazon SES | email-smtp.<region>.amazonaws.com |
IAM SMTP username | IAM SMTP password |
| SendGrid | smtp.sendgrid.net |
apikey |
your API key |
| Resend | smtp.resend.com |
resend |
your API key |
Three things that cause most failures
SMTP_SECUREnot matching the port. A mismatch doesn't error — it hangs and times out, which looks like a network problem.- Google revoking the app password. Google invalidates every app password when that account's main password changes. Email works for months, then stops with no warning.
- Microsoft 365 blocking authentication. SMTP AUTH is disabled by default and must be enabled per mailbox. This is the most common failure for corporate senders.
Who processes the mail
Whoever relays the notifications is processing their content — outage titles, locations and resolution notes. If you handle patient information, that requires a Business Associate Agreement. Your existing mail provider is usually the safer choice, since a covered entity almost certainly already holds one with them; transactional providers need a separate BAA that not all offer.
Security note
SMTP_PASS sits in .env as plain text.
Restrict the file's permissions, keep it out of version control, and
don't paste its contents into tickets or chat.
Demo mode
| Variable | Default | Purpose |
|---|---|---|
DEMO_MODE |
false |
Auto-seed fictional sample data on first-run setup. Same effect as
the "Load demo data" setup checkbox. Purge via
POST /api/demo/purge. |
Feature flags (implicit)
There are no on/off switches — each feature is "configured" or not, based on its own environment variables. Unconfigured features hide themselves.
| Feature | Enabled when | Disables cleanly when off |
|---|---|---|
| Email notifications | SMTP_HOST set |
No emails sent; subscribe hint says so |
| Google Chat | GOOGLE_CHAT_WEBHOOK_URL set |
No chat posts |
| PagerDuty tile | PAGERDUTY_USER_API_KEY set |
Tile hidden on wall; endpoint returns
NOT_CONFIGURED |
| OPManager tile | OPMANAGER_API_URL + token/key |
Tile hidden |
| Zoho tile | Zoho OAuth trio + org id | Tile hidden |
| Power outage tile | clinics_fips.json non-empty |
Tile hidden |
| Circuit Incidents tile | PagerDuty + Circuit Provider service + clinic map | Tile hidden |
GET /api/public-config reports the current feature state
(no secrets) so custom dashboards can adapt too.
Integrations
See INTEGRATIONS.md for per-integration variable lists.
Licensing
| Variable | Default | Purpose |
|---|---|---|
OTB_LICENSE_PUBLIC_KEY |
built with your release | ed25519 public key verifying license keys. Change only when rotating keys. |
LICENSE_KEY |
(empty) | Supply the license key inline instead of
data/license.key (env wins over file) |
OTB_LICENSE_FILE |
(empty) | Alternate path for the license key file |
Locations
The board ships with a small generic location list
(seed/locations.json) loaded on first run when the database
has none. Replace it with your own:
{
"locations": [
{ "name": "Headquarters", "type": "office", "state": "" },
{ "name": "Branch Office — North", "type": "office", "state": "" },
{ "name": "Microsoft Azure", "type": "cloud", "state": "" }
]
}
type is free-form but the UI has icons for
office, datacenter, cloud,
wfh. Point SEED_LOCATIONS_PATH at a different
file if you prefer to keep the shipped sample untouched.
Locations are otherwise managed in the app (Admin → Locations) after setup.
Runtime state layout
data/
db.json # the database (atomic writes)
backups/ # hourly-ish backups, newest 10 kept
license.key # your license key (mode 0600)
license-state.json # trial start timestamp
DATA_DIR relocates all of it. Back up this directory and
you can rebuild the instance anywhere.