OTBOperational Transparency Board

Installation

Three supported paths. Pick one:

Path Best for
Windows installer (install.ps1) Windows Server / desktop box, no Docker
Docker Compose Any Linux host or container platform
Zip + systemd Linux hosts without Docker

All paths end the same way: open the board in a browser and complete first-run setup (create the admin account).


1. Windows installer

Prerequisites: Node.js 18 or newer. Without it, add -DownloadNode and the installer fetches the current LTS MSI for you.

From an elevated PowerShell in the application folder:

powershell -ExecutionPolicy Bypass -File .\install.ps1

Useful variants:

.\install.ps1 -DownloadNode        # install Node LTS automatically if missing
.\install.ps1 -Port 8080           # listen on a different port

What the installer does:

  1. Verifies Node ≥ 18 (downloads LTS MSI when -DownloadNode is passed).
  2. Creates .env containing a freshly generated SESSION_SECRET.
  3. Runs npm ci --omit=dev.
  4. Registers the OTB-Server Scheduled Task (starts at boot, restarts on failure) and starts it immediately.

Re-running the installer is safe: it replaces the Scheduled Task and leaves your .env, database, and license key alone.

Uninstall

powershell -ExecutionPolicy Bypass -File .\uninstall.ps1

This stops the app and removes the Scheduled Task. Files, database, and your license key stay on disk. To remove everything, add -RemoveFiles — it will ask you to type DELETE before touching data.


2. Docker Compose

cp .env.example .env      # then edit: set SESSION_SECRET at minimum
docker compose up -d

The compose file builds the image, mounts a named volume at /app/data, and maps port 3010. All runtime state (database, backups, license key) lives on that volume — back it up and the whole instance moves with it.

Update:

docker compose pull       # if you consume a prebuilt image
docker compose build --no-cache && docker compose up -d

3. Zip + systemd (Linux)

# as root
mkdir -p /opt/otb && cd /opt/otb
unzip /path/to/operational-transparency-board.zip
cp .env.example .env      # then edit: set SESSION_SECRET at minimum
npm ci --omit=dev

useradd -r -s /usr/sbin/nologin otb || true
chown -R otb:otb /opt/otb

cp deploy/otb.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now otb

The unit expects Node at /usr/bin/node — adjust ExecStart if yours is elsewhere. Logs go to the journal: journalctl -u otb -f.


First-run setup

Open http://<host>:3010. The board prompts you to create the first admin account (username, display name, email, password ≥ 10 characters).

Optional checkbox: Load demo data — fills the board with fictional sample incidents so you can see it populated before creating real ones. Demo data is purgeable any time (Admin → purge, or POST /api/demo/purge).

Behind a reverse proxy (optional)

Terminate TLS at your proxy (IIS, nginx, Caddy, Cloudflare tunnel) and point it at http://<host>:3010. Set PUBLIC_BOARD_URL in .env to the public URL so emailed links are correct:

PUBLIC_BOARD_URL=https://status.example.com

Setting up email

Do this before you go live. Email notifications are how staff find out an incident is happening — without SMTP configured, the board works perfectly but sends nothing, and nobody is told.

The board ships with email silently disabled. You configure it entirely through environment variables in .env, then restart the service.

Two ways to send

Relay through your own mail provider Use a transactional provider
Example Google Workspace, Microsoft 365, Exchange SendGrid, Amazon SES, Mailgun, Postmark, Resend
Setup effort Low — credentials from an existing mailbox Medium — DNS records, domain verification
Sending limits Mailbox limits (often 500–2,000/day) Plan-dependent, usually far higher
Deliverability Good, if your domain is already authenticated Very good, once SPF/DKIM are set
Who processes the content Your existing mail provider A third party

If you handle patient information, read this. Notification emails contain outage titles, locations and resolution notes. Whoever relays them is processing that content, so you need a Business Associate Agreement with them.

Your existing mail provider is usually the safer choice — if you're a covered entity, you almost certainly already have a BAA with Google or Microsoft. A transactional provider generally needs a separate BAA, and not all of them offer one at any price. Check before you choose.

The variables

Add these to .env (see .env.example for the annotated template):

SMTP_HOST=smtp.example.com   # your provider's SMTP server
SMTP_PORT=587                # 587 = STARTTLS (usual), 465 = implicit TLS
SMTP_USER=you@yourdomain.com # the account that authenticates
SMTP_PASS=...                # password or app password — see below
SMTP_SECURE=false            # set true only for port 465
SMTP_EHLO_NAME=              # usually leave blank
SMTP_FROM=                   # From address; blank = "ORG_NAME <SMTP_USER>"
OPERATIONS_DL=               # optional: a distribution list copied on every alert

SMTP_SECURE must match the port: false for 587 (STARTTLS, the common case), true for 465 (implicit TLS). Getting this wrong is the most common cause of a connection that hangs and then times out.

Common providers

Provider SMTP_HOST Port SMTP_USER SMTP_PASS
Google Workspace smtp.gmail.com 587 full address App password (see below)
Microsoft 365 smtp.office365.com 587 full address Account password or app password
Exchange on-premises your server 587 / 25 depends on relay config often blank (IP allow-list)
Amazon SES email-smtp.<region>.amazonaws.com 587 SMTP username from IAM SMTP password from IAM
SendGrid smtp.sendgrid.net 587 literally apikey your API key
Mailgun smtp.mailgun.org 587 SMTP login from domain settings SMTP password
Postmark smtp.postmarkapp.com 587 Server Token Server Token
Resend smtp.resend.com 587 literally resend your API key

Google Workspace

  1. The sending account needs 2-Step Verification enabled — app passwords cannot be created without it.
  2. Go to myaccount.google.com → Security → 2-Step Verification → App passwords.
  3. Create one named something like Operational Transparency Board. Google shows a 16-character password once — copy it immediately.
  4. Use your full email address as SMTP_USER and that 16-character value as SMTP_PASS.

⚠️ The trap that catches everyone: Google revokes all app passwords whenever the account's main password changes. Email will work for months and then stop dead, with no warning. If your board suddenly stops sending, check this first.

⚠️ Your Workspace administrator may have disabled app passwords entirely for the organization. If the option isn't there, you cannot use this route — use a transactional provider instead.

Microsoft 365

Microsoft disables SMTP authentication by default, and this is the single most common reason email will not work. Enabling it is a per-mailbox setting.

  1. Microsoft 365 admin centre → Users → Active users → select the sending account.
  2. Open the Mail tab → Manage email apps.
  3. Tick Authenticated SMTP → Save.
  4. In .env, use smtp.office365.com, port 587, with the mailbox address as SMTP_USER.

If you prefer to set this in bulk, there is a PowerShell equivalent — search Microsoft's documentation for "enable SMTP AUTH for a mailbox", as the cmdlet has changed over time.

If authentication still fails, the tenant may enforce a Conditional Access policy blocking legacy authentication. In that case use a transactional provider — Microsoft has been steadily retiring basic SMTP authentication.

Transactional providers

Any of SendGrid, SES, Mailgun, Postmark or Resend will work — the board speaks plain SMTP. Two things they all need:

  1. Verify your sending domain in their dashboard.
  2. Add the DNS records they give you — see below.

Amazon SES starts in sandbox mode, which only permits sending to verified addresses. You must request production access before it will send to your staff. Budget a day for approval.


DNS: SPF, DKIM and DMARC

Sending from no-reply@yourdomain.com through a third party requires DNS records at your domain, or your notifications will land in spam — or be rejected outright. This step is not optional and is easy to forget.

Record Purpose Where
SPF Authorises the sending service for your domain A TXT record listing the provider
DKIM Cryptographically signs your mail Provider gives you the exact record
DMARC Tells receivers what to do when SPF/DKIM fail A TXT record at _dmarc.yourdomain.com

Your provider's dashboard generates the exact values — copy them into your DNS host. If you relay through Google Workspace or Microsoft 365, your domain is usually already authenticated and you can skip this.


Verify it works

After editing .env, restart the service — environment variables are read only at startup.

# Docker
docker compose restart

# Windows
Restart-Service OperationalTransparencyBoard

# systemd
sudo systemctl restart otb

Then confirm the configuration was picked up. The startup log reports SMTP status:

[CONFIG] SMTP: smtp.gmail.com:587

If it says not configured, the variables aren't reaching the process — check that you edited the right .env and restarted.

The real test: create a test incident on the board and check that a notification arrives. Remember that subscriber emails only go to verified subscribers, and that a subscriber only receives incidents matching their chosen locations and severities — so if nothing arrives, check those before assuming SMTP is broken.

No test-email button yet. Verification means creating an incident and seeing what lands. If you have no subscribers, add yourself as one first — you'll need to click the link in the verification email, which conveniently proves the outbound path works.


Troubleshooting

The error string is the fastest way to the cause. Common ones:

Error Cause Fix
535-5.7.8 Username and Password not accepted … BadCredentials Google app password wrong, or revoked after a password change Create a fresh app password
535 5.7.139 Authentication unsuccessful Microsoft 365 — SMTP AUTH is disabled Enable Authenticated SMTP on the mailbox
Connection timeout Wrong port, or SMTP_SECURE doesn't match the port 587 → SMTP_SECURE=false; 465 → true
421 4.7.0 … Try again later Provider rate-limiting or blocking the source Check sending limits; check the source IP isn't blocked
550 … not allowed to send Sending address not authorised by the provider Verify the From address / domain with the provider
self signed certificate On-premises relay with an untrusted certificate Use your internal CA, or the provider's correct hostname
Mail sends but lands in spam Missing SPF/DKIM/DMARC Add the DNS records — see above
not configured in the startup log .env not read, or service not restarted Verify the file path and restart

Because the SMTP password is stored in .env as plain text, treat that file as sensitive. Restrict its permissions, keep it out of version control, and don't paste its contents into tickets or chat.


License activation

  1. Buy a license; a key arrives by email (OTB1.…).
  2. Log in to the board as an administrator.
  3. Open License (or go to /license.html).
  4. Paste the key → Activate. It is verified against the product's public key before anything is saved — a typo never overwrites a working key.

State after activation:

State Meaning
valid Licensed; everything works
trial 30-day trial running; everything works
trial_expired Trial over; public board readable, admin actions paused
invalid Key unreadable/unsigned; same soft-lock as expired

The soft-lock never blocks the public board — your users always see status.

Where the key is stored: data/license.key next to the app (in Docker, on the /app/data volume). You can also supply it via the OTB_LICENSE_PUBLIC_KEY / LICENSE_KEY environment variables — see CONFIGURATION.md.

Moving servers? Copy the data directory. The license key and trial state travel with it.


Updating the application

Back up the data directory first — it contains everything that matters.

  1. Stop the service (Stop-ScheduledTask OTB-Server / docker compose stop / systemctl stop otb).
  2. Replace the application files (keep data/, .env, and db.json).
  3. npm ci --omit=dev (non-Docker installs).
  4. Start the service again.

Your license covers updates through the updatesUntil date printed on your license (visible in the License page). After it lapses the app keeps working forever — you just stop receiving new builds until you renew.