Documentation

Everything you need to deploy, configure, and operate Relay — self-hosted, enterprise-grade uptime monitoring with a built-in public status page.

Quick Start

One-liner (no config file)

docker run -d \
  --name relay \
  --restart unless-stopped \
  -p 8080:8080 \
  -v relay-data:/data \
  -e RELAY_SECRET=$(openssl rand -hex 16) \
  -e RELAY_SITE_NAME="Acme Status" \
  -e RELAY_SITE_URL="https://status.example.com" \
  -e RELAY_ADMIN_PASS=yourpassword \
  ghcr.io/rohzzn/relay

Docker Compose + Caddy (auto-TLS, recommended)

git clone https://github.com/rohzzn/relay
cd relay
cp .env.example .env
# Edit .env — set RELAY_SECRET, RELAY_ADMIN_PASS, RELAY_SITE_NAME, RELAY_SITE_URL
# Edit Caddyfile — replace status.example.com with your domain

docker compose up -d

Caddy handles Let's Encrypt automatically. No certificate configuration needed.

Build from source

git clone https://github.com/rohzzn/relay
cd relay
go build -o relay ./cmd/relay

RELAY_SECRET=secret RELAY_ADMIN_PASS=admin ./relay

Requires Go 1.22+. No other dependencies.

Configuration

All settings are environment variables. Only RELAY_SECRET and RELAY_ADMIN_PASS are required:

VariableRequiredDefaultDescription
RELAY_SECRETYesHMAC key for session cookies. Generate with openssl rand -hex 16
RELAY_ADMIN_PASSYesAdmin dashboard password
RELAY_ADMIN_USERadminAdmin username
RELAY_SITE_NAMEStatusDisplayed on the public status page header
RELAY_SITE_URLhttp://localhost:8080Full public URL — used in email links and heartbeat endpoint display
RELAY_LOGO_URLLogo image URL shown on the status page header
RELAY_FOOTER_TEXTPowered by RelayCustom footer text for white-labelling the status page
RELAY_SMTP_HOSTSMTP server hostname for subscriber confirmation emails
RELAY_SMTP_PORT587SMTP port
RELAY_SMTP_USERSMTP username
RELAY_SMTP_PASSSMTP password
RELAY_SMTP_FROMFrom address for outgoing emails
RELAY_DATA./dataDirectory for relay.db
RELAY_PORT8080HTTP listen port
RELAY_CHECK_CONCURRENCY20Max concurrent check goroutines
RELAY_RETENTION_DAYS90Days of check history to keep
No SMTP? Subscriber confirmations are auto-approved, so email subscriptions work during local testing without an SMTP server.

Monitor Types

TypeTargetExample
httpURLhttps://api.example.com/health
tcphost:portdb.internal:5432
tlshostnameapi.example.com
dnshostnameexample.com
heartbeatautocron jobs POST to Relay

Per-monitor options

These fields are available on the monitor form and stored as JSON in the config column:

OptionApplies toDescription
timeout_shttp, tcp, tls, dnsRequest timeout in seconds (default 30)
expected_statushttpRequire a specific HTTP status code (e.g. 200). Blank = any 2xx/3xx passes.
keywordhttpResponse body must contain this string, otherwise the monitor is marked degraded
max_latency_mshttpMark the monitor degraded if response time exceeds this threshold in milliseconds
warn_daystlsMark degraded when the certificate expires in ≤ N days (default 14)
expected_ipdnsAlert if the resolved IP address doesn't match this value

Pause & Test Now

Every monitor card has two quick actions:

Heartbeat Monitors

A heartbeat monitor expects your cron job to POST to Relay on each successful run. If the ping stops arriving within the check interval, Relay opens an incident.

After creating a heartbeat monitor, the dashboard shows the exact endpoint to call:

POST https://status.example.com/ping/{monitor-id}

Example crontab entry:

*/5 * * * * /path/to/job && curl -sS -X POST https://status.example.com/ping/YOUR_MONITOR_ID
The heartbeat target is automatically set to the monitor's own ID when you create it. You don't need to fill in the target field manually.

Monitor Groups

Assign a Group / Category to each monitor on its form (e.g. API, Database, CDN). Monitors with the same group name are displayed together as a labelled section on both the admin dashboard and the public status page — identical to enterprise Statuspage.io.

Alert Channels

Configure channels in Admin → Alert Channels. Each channel has a Send Test button to fire a dummy alert and verify the connection before relying on it.

TypeConfigNotes
Webhook Any HTTPS URL Relay POSTs JSON with event, monitor, status, detail, latency_ms, time, and incident_id
Slack Incoming webhook URL Rich attachment with status colour, fields for monitor name, target, status, and latency
Email SMTP host, port, user, pass, from, to Uses its own SMTP config — independent of RELAY_SMTP_* env vars used for subscriber emails
PagerDuty Integration key Uses Events API v2. Create an "Events API v2" integration in your PagerDuty service and paste the key. Down events trigger, up events resolve by alias.
OpsGenie API key, region (US / EU) Uses the OpsGenie Alerts API. Down events open by alias, up events close. EU endpoint is automatically used when region is set to EU.

Alerts have a built-in 10-minute cooldown per monitor to prevent alert storms. Recovery ("back up") notifications always send immediately, bypassing the cooldown.

Per-monitor routing

By default, all configured channels fire for every monitor. On the monitor create/edit form, check specific channels under Alert Channels to route only those — for example, database alerts to PagerDuty and API alerts to Slack. Leave all boxes unchecked to use all channels (the default).

Maintenance Windows

Schedule maintenance windows in Admin → Maintenance to suppress alerts during planned downtime such as deployments, migrations, or infrastructure changes.

Times are entered in your browser's local timezone. The server stores them as UTC Unix timestamps.

Team & RBAC

Invite team members in Admin → Team. Each user gets a role that controls what they can do:

RolePermissions
Admin Full access — manage monitors, incidents, channels, maintenance windows, team members, and API keys
Editor Manage monitors, post incident updates, create and delete maintenance windows
Viewer Read-only access to the admin dashboard — cannot change anything

Team member passwords are hashed with bcrypt. The root admin account (RELAY_ADMIN_USER / RELAY_ADMIN_PASS) always has full access and cannot be removed through the UI. Role changes take effect on the user's next page load.

REST API

Relay exposes a REST API at /api/v1/. Use it for CI/CD pipelines, Infrastructure-as-Code, Grafana datasources, or any automation.

Authentication

Create API keys in Admin → Settings. Each key has a role (Admin, Editor, or Viewer) that controls what it can do. Pass the key as a Bearer token:

curl -H "Authorization: Bearer relay_..." \
     https://status.example.com/api/v1/monitors

Admin session cookies also authenticate API requests, so the API works from the browser too.

API keys are shown only once when created. Store them securely — they cannot be retrieved later. Keys can be revoked from Settings at any time.

Endpoints

MethodPathDescription
GET/api/v1/statusOverall status string (operational, degraded, outage) and active incident count
GET/api/v1/monitorsList all monitors with 24h/7d/30d/90d uptime percentages and last check result
POST/api/v1/monitorsCreate a monitor. Body: {"name","type","target","interval_s","group","config":{}}
GET/api/v1/monitors/:idGet a single monitor
PUT/api/v1/monitors/:idUpdate a monitor. Partial updates supported — only send the fields you want to change.
DELETE/api/v1/monitors/:idDelete a monitor and all its check history
GET/api/v1/monitors/:id/metricsTime-bucketed latency data. Query param: ?hours=24 (default) up to ?hours=720
GET/api/v1/incidentsList incidents. Add ?active=true to return only unresolved incidents.
POST/api/v1/incidentsCreate an incident. Body: {"title","status","body","monitor_id"}

Example: create a monitor

curl -X POST \
  -H "Authorization: Bearer relay_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Payments API","type":"http","target":"https://pay.example.com/health","interval_s":60,"group":"API"}' \
  https://status.example.com/api/v1/monitors

Example: pull latency metrics

curl -H "Authorization: Bearer relay_..." \
  "https://status.example.com/api/v1/monitors/MONITOR_ID/metrics?hours=24"

Returns an array of {"bucket": unix_ts, "avg_ms": float, "check_count": int} points bucketed at 15-minute intervals for ≤24h or 1-hour intervals for longer windows.

Audit Log

Every admin action is recorded in the audit log, accessible at Admin → Audit Log. Each entry captures:

The log is append-only and paginated at 50 entries per page. It cannot be cleared through the UI.

Architecture

relay/
├── cmd/relay/          Entry point, graceful shutdown, healthcheck subcommand
├── internal/
│   ├── config/         Environment-based configuration (env vars only)
│   ├── db/             SQLite (WAL mode), all queries — no ORM
│   ├── check/          HTTP · TCP · TLS · DNS · Heartbeat checkers
│   ├── scheduler/      Per-monitor goroutines with semaphore concurrency pool
│   ├── state/          FSM: up/degraded/down, auto incident open/close
│   ├── alert/          Cooldown-aware dispatcher with per-monitor channel routing
│   ├── notify/         Slack · SMTP · Webhook · PagerDuty · OpsGenie adapters
│   └── server/         HTTP handlers · WebSocket hub · HMAC session auth · REST API
└── web/
    ├── templates/       html/template pages — embedded in the binary
    └── static/          CSS (dark mode) + JS — embedded in the binary

Why Go?

Single static binary, goroutine-per-monitor scheduler, ~15 MB Docker image. Uptime Kuma is 200 MB and people notice.

Why SQLite WAL?

Zero ops. Back up with cp relay.db relay.db.bak. Handles dozens of concurrent readers with WAL mode. No Postgres to run, no connection pool to tune.

Why HTMX?

The live dashboard has real-time updates with zero client-side framework. No React, no build step — the dashboard loads in under 300ms on a Raspberry Pi and the entire admin UI weighs less than 50 KB.

Database schema

Key tables and their purpose:

TablePurpose
monitorsConfigured checks. Includes group_name and paused columns.
checksIndividual probe results — status, latency, detail, timestamp. Pruned at RELAY_RETENTION_DAYS.
incidentsDowntime events with timeline body. Auto-opened and closed by the state machine.
alert_channelsDelivery targets (Slack, webhook, email, PagerDuty, OpsGenie).
monitor_channelsMany-to-many join between monitors and channels for per-monitor routing.
maintenance_windowsScheduled alert suppression windows.
usersTeam members with bcrypt passwords and roles.
api_keysSHA-256-hashed API keys with role and last-used timestamp.
subscribersStatus page email subscribers with confirmation tokens.
heartbeatsLast ping timestamp per heartbeat monitor.
audit_logAppend-only record of all admin actions.

Roadmap

Contributing

  1. Fork the repo
  2. go run ./cmd/relay — starts with live template reloading
  3. Templates are in web/templates/, styles in web/static/style.css
  4. Open a PR — all contributions welcome

Code style: Standard gofmt. No ORM — queries live in internal/db/db.go as plain SQL. No external router — Go 1.22 stdlib mux only. No frontend build step.

License

MIT — see LICENSE on GitHub.

Relay is not affiliated with Uptime Kuma or Statuspage.io.