OTBOperational Transparency Board

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

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

  1. SMTP_SECURE not matching the port. A mismatch doesn't error — it hangs and times out, which looks like a network problem.
  2. 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.
  3. 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.