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
- Status page:
http://localhost:8080 - Admin dashboard:
http://localhost:8080/admin(login:admin/ your password) - REST API:
http://localhost:8080/api/v1/monitors
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:
| Variable | Required | Default | Description |
|---|---|---|---|
RELAY_SECRET | Yes | — | HMAC key for session cookies. Generate with openssl rand -hex 16 |
RELAY_ADMIN_PASS | Yes | — | Admin dashboard password |
RELAY_ADMIN_USER | admin | Admin username | |
RELAY_SITE_NAME | Status | Displayed on the public status page header | |
RELAY_SITE_URL | http://localhost:8080 | Full public URL — used in email links and heartbeat endpoint display | |
RELAY_LOGO_URL | — | Logo image URL shown on the status page header | |
RELAY_FOOTER_TEXT | Powered by Relay | Custom footer text for white-labelling the status page | |
RELAY_SMTP_HOST | — | SMTP server hostname for subscriber confirmation emails | |
RELAY_SMTP_PORT | 587 | SMTP port | |
RELAY_SMTP_USER | — | SMTP username | |
RELAY_SMTP_PASS | — | SMTP password | |
RELAY_SMTP_FROM | — | From address for outgoing emails | |
RELAY_DATA | ./data | Directory for relay.db | |
RELAY_PORT | 8080 | HTTP listen port | |
RELAY_CHECK_CONCURRENCY | 20 | Max concurrent check goroutines | |
RELAY_RETENTION_DAYS | 90 | Days of check history to keep |
Monitor Types
| Type | Target | Example |
|---|---|---|
http | URL | https://api.example.com/health |
tcp | host:port | db.internal:5432 |
tls | hostname | api.example.com |
dns | hostname | example.com |
heartbeat | auto | cron jobs POST to Relay |
Per-monitor options
These fields are available on the monitor form and stored as JSON in the config column:
| Option | Applies to | Description |
|---|---|---|
timeout_s | http, tcp, tls, dns | Request timeout in seconds (default 30) |
expected_status | http | Require a specific HTTP status code (e.g. 200). Blank = any 2xx/3xx passes. |
keyword | http | Response body must contain this string, otherwise the monitor is marked degraded |
max_latency_ms | http | Mark the monitor degraded if response time exceeds this threshold in milliseconds |
warn_days | tls | Mark degraded when the certificate expires in ≤ N days (default 14) |
expected_ip | dns | Alert if the resolved IP address doesn't match this value |
Pause & Test Now
Every monitor card has two quick actions:
- Pause / Resume — stops checks immediately without deleting the monitor. The monitor shows as paused on the dashboard and is hidden from the public status page.
- Test Now — fires a single probe immediately and shows the result inline. Also available as a button on the monitor edit form. Useful for verifying a new monitor before relying on it.
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
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.
- Monitors with no group are shown under Ungrouped on the dashboard and Services on the status page.
- Groups appear in the order their first monitor was created.
- You can rename a group by editing all monitors in it.
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.
| Type | Config | Notes |
|---|---|---|
| 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 |
| 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.
- During an active window, monitors continue running and check results are recorded — but no incidents are auto-created and no alert channels fire.
- Windows can target all monitors or a specific monitor.
- The dashboard and status page continue to reflect real monitor status — the window only suppresses alerting.
- Completed, upcoming, and active windows are listed with their time range and scope.
Team & RBAC
Invite team members in Admin → Team. Each user gets a role that controls what they can do:
| Role | Permissions |
|---|---|
| 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.
Endpoints
| Method | Path | Description |
|---|---|---|
GET | /api/v1/status | Overall status string (operational, degraded, outage) and active incident count |
GET | /api/v1/monitors | List all monitors with 24h/7d/30d/90d uptime percentages and last check result |
POST | /api/v1/monitors | Create a monitor. Body: {"name","type","target","interval_s","group","config":{}} |
GET | /api/v1/monitors/:id | Get a single monitor |
PUT | /api/v1/monitors/:id | Update a monitor. Partial updates supported — only send the fields you want to change. |
DELETE | /api/v1/monitors/:id | Delete a monitor and all its check history |
GET | /api/v1/monitors/:id/metrics | Time-bucketed latency data. Query param: ?hours=24 (default) up to ?hours=720 |
GET | /api/v1/incidents | List incidents. Add ?active=true to return only unresolved incidents. |
POST | /api/v1/incidents | Create 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:
- Actor — the username (or
apifor API key actions) - Action — what was done:
create,update,delete,pause,resume,resolve,update_role - Entity type —
monitor,incident,channel,user,api_key,maintenance_window - Detail — the name or relevant identifier of the entity
- Timestamp — when the action happened
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:
| Table | Purpose |
|---|---|
monitors | Configured checks. Includes group_name and paused columns. |
checks | Individual probe results — status, latency, detail, timestamp. Pruned at RELAY_RETENTION_DAYS. |
incidents | Downtime events with timeline body. Auto-opened and closed by the state machine. |
alert_channels | Delivery targets (Slack, webhook, email, PagerDuty, OpsGenie). |
monitor_channels | Many-to-many join between monitors and channels for per-monitor routing. |
maintenance_windows | Scheduled alert suppression windows. |
users | Team members with bcrypt passwords and roles. |
api_keys | SHA-256-hashed API keys with role and last-used timestamp. |
subscribers | Status page email subscribers with confirmation tokens. |
heartbeats | Last ping timestamp per heartbeat monitor. |
audit_log | Append-only record of all admin actions. |
Roadmap
- ✓ HTTP, TCP, TLS, DNS, Heartbeat monitors
- ✓ Live dashboard via WebSocket with sparklines
- ✓ 90-day uptime bars
- ✓ Grouped monitor sections
- ✓ Pause / resume monitors
- ✓ Test Now button
- ✓ Incident management with timeline updates
- ✓ Public incident history page
- ✓ Email subscriber list with confirmation
- ✓ Alert channels — Slack, webhook, SMTP email
- ✓ PagerDuty and OpsGenie integrations
- ✓ Per-monitor alert channel routing
- ✓ Send Test button per channel
- ✓ Maintenance windows
- ✓ Team RBAC (Admin / Editor / Viewer)
- ✓ REST API v1 with scoped API keys
- ✓ Audit log
- ✓ Dark mode
- ✓ Custom 404/500 pages
- ✓ Docker healthcheck subcommand
- v2:
relay-probe— lightweight agent binary for multi-region monitoring - v2: Response time history graphs in the dashboard
- v3: On-call schedule with rotating recipients
- v3: SSO (OIDC)
Contributing
- Fork the repo
go run ./cmd/relay— starts with live template reloading- Templates are in
web/templates/, styles inweb/static/style.css - 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.