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:
- Verifies Node ≥ 18 (downloads LTS MSI when
-DownloadNodeis passed). - Creates
.envcontaining a freshly generatedSESSION_SECRET. - Runs
npm ci --omit=dev. - 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
- The sending account needs 2-Step Verification enabled — app passwords cannot be created without it.
- Go to myaccount.google.com → Security → 2-Step Verification → App passwords.
- Create one named something like
Operational Transparency Board. Google shows a 16-character password once — copy it immediately. - Use your full email address as
SMTP_USERand that 16-character value asSMTP_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.
- Microsoft 365 admin centre → Users → Active users → select the sending account.
- Open the Mail tab → Manage email apps.
- Tick Authenticated SMTP → Save.
- In
.env, usesmtp.office365.com, port587, with the mailbox address asSMTP_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:
- Verify your sending domain in their dashboard.
- 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
- Buy a license; a key arrives by email (
OTB1.…). - Log in to the board as an administrator.
- Open License (or go to
/license.html). - 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.
- Stop the service (
Stop-ScheduledTask OTB-Server/docker compose stop/systemctl stop otb). - Replace the application files (keep
data/,.env, anddb.json). npm ci --omit=dev(non-Docker installs).- 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.