A lightweight real-time event streaming service. Any tool that can POST JSON gets a live dashboard — browser UI, TUI, or both.
Most observability tools are heavyweight — they need databases, collectors, dashboards, and configuration before you see anything. eventrelay is the opposite: a single binary that gives you a real-time event feed in seconds. It's designed for development workflows, CI pipelines, agent monitoring, and anywhere you want visibility without infrastructure.
- Zero dependencies — single Go binary, no database required
- Language-agnostic — POST JSON from any language or tool, SDKs for Go, Python, and TypeScript
- Fire-and-forget — SDKs never block your application's critical path
- Two dashboards — web UI in the browser, TUI in the terminal
- Notification routing — match rules forward events to Slack, Discord, webhooks, or a database
go install github.com/dmoose/eventrelay@latestOr build from source:
git clone https://github.com/dmoose/eventrelay.git
cd eventrelay
make buildeventrelay --port 6060Open http://localhost:6060 in a browser, then send events:
eventrelay send -s myapp -a deploy -d '{"env":"prod"}'Or with curl:
curl -X POST http://localhost:6060/events \
-d '{"source":"myapp","action":"deploy","level":"info","data":{"env":"prod"}}'Connect a terminal dashboard to a running server:
eventrelay --tui
eventrelay --tui --url http://remote-server:6060Keys: / filter, x clear filter, p pause, c clear, q quit, ctrl+c force quit.
Send events from scripts, cron jobs, or the terminal without curl:
eventrelay send -s myapp -a deploy -l info -d '{"branch":"main"}'
eventrelay send --source ci --action build_done --channel builds
eventrelay send -s myapp -a crash -l error
# Pipe raw JSON
echo '{"source":"ci","action":"done"}' | eventrelay send --stdin
# With auth and custom server
eventrelay send -s myapp -a test -t mysecret -p 8080
eventrelay send -s myapp -a test --url http://remote:6060Events are JSON objects posted to POST /events:
{
"source": "myapp",
"channel": "deploy",
"action": "build_complete",
"level": "info",
"agent_id": "claude-code",
"duration_ms": 4200,
"data": {"branch": "main", "commit": "abc123"},
"ts": "2026-03-17T12:00:00Z"
}| Field | Type | Required | Description |
|---|---|---|---|
source |
string | yes | What system or tool sent this event. Use a consistent identifier like myapp, ci, llmshadow. This is the primary grouping key in the dashboard. |
channel |
string | no | A topic or category within the source. Use to separate concerns like deploy, builds, monitoring. Events can be filtered by channel via tabs in the dashboard. |
action |
string | no | What happened. Use a short verb or operation name like started, completed, db_query, shadow_scan. |
level |
string | no | Severity: debug, info, warn, or error. Defaults to info. The dashboard color-codes events by level and shows error/warn counts. |
agent_id |
string | no | Identifies which agent, worker, or instance emitted the event. Useful when multiple agents share the same source — e.g., claude-code-session-1, worker-3. Displayed in the dashboard and filterable. |
duration_ms |
integer | no | How long the operation took in milliseconds. Displayed inline in the dashboard. Use the SDK Timed() helpers to set this automatically. |
data |
object | no | Arbitrary JSON payload with additional context. Shown inline in the dashboard for small payloads, expandable for larger ones. |
ts |
string | no | ISO 8601 timestamp. Auto-set to the server's current time if omitted. |
seq |
integer | — | Assigned by the server. Monotonically increasing sequence number. Do not set this. |
- Always set
sourceto identify yourself consistently across events - Set
agent_idto distinguish between concurrent instances of the same source - Use
actionfor the operation name, not a full sentence — keep it grep-friendly - Use
level: errorfor failures,level: warnfor degraded states,level: debugfor verbose tracing - Put structured details in
data, not inaction— the action should be a stable key you can filter on - Use
channelto separate event streams within a source (e.g., a CI system might use channelsbuild,test,deploy)
| Endpoint | Method | Description |
|---|---|---|
/events |
POST | Submit an event |
/events/batch |
POST | Submit multiple events as a JSON array |
/events/stream |
GET | SSE stream (filterable via query params) |
/events/recent |
GET | Last N events as JSON (?n=100) |
/events/stats |
GET | Aggregate counters (by source, level, channel) |
/events/rate |
GET | Event rate history (?minutes=5&buckets=60) |
/events/channels |
GET | List all active channels |
/healthz |
GET | Health check ({"ok":true,"version":"..."}) |
/ |
GET | Web dashboard |
SSE and recent endpoints accept filter params: ?source=x&channel=y&level=error&action=z&agent_id=a
POST /events/batch is processed sequentially and is non-atomic: if a later event is invalid, earlier valid events in the same batch may already be accepted.
import "github.com/dmoose/eventrelay/client"
c := client.New("http://localhost:6060/events", "myapp")
c.Emit("deploy", map[string]any{"env": "prod"})
// Timed operations
done := c.Timed("db_query", nil)
// ... do work ...
done(map[string]any{"rows": 42})
c.Flush() // wait for pending events before exithandler := client.NewSlogHandler(c, "logs")
logger := slog.New(handler)
logger.Info("request handled", "path", "/api/users", "status", 200)See client/README.md for full Go SDK documentation.
from eventrelay import Client
er = Client("http://localhost:6060/events", "myapp")
er.emit("deploy", {"env": "prod"})
with er.timed("db_query") as t:
result = do_query()
t.data["rows"] = len(result)
er.flush()See sdks/python/README.md for full Python SDK documentation.
import { Client } from "eventrelay";
const er = new Client("http://localhost:6060/events", "myapp");
er.emit("deploy", { env: "prod" });
const done = er.timed("db_query");
const result = await doQuery();
done({ rows: result.length });
await er.flush();See sdks/typescript/README.md for full TypeScript SDK documentation.
eventrelay can execute local commands and display their output as dashboard pages. This turns it into a portal for any CLI tool on the system — anything that can produce text, JSON, YAML, or markdown becomes a browser-accessible dashboard tab.
Add a pages section to your config file:
server:
scripts_dir: /usr/local/share/eventrelay/scripts
pages:
- name: System
command: er-system
format: markdown
interval: 30s
- name: Ports
command: er-ports
format: text
interval: 10s
- name: Homebrew
command: er-brew
format: markdown
interval: 5m| Format | Rendering |
|---|---|
text |
Pre-formatted monospace, HTML-escaped |
json |
Syntax-highlighted with color-coded keys, strings, numbers |
yaml |
Pre-formatted monospace, HTML-escaped |
markdown |
Rendered with headings, bold, code blocks, lists, tables, blockquotes |
The scripts/ directory contains ready-to-use page scripts, installed to $PREFIX/share/eventrelay/scripts/ by make install:
| Script | Format | Description |
|---|---|---|
er-system |
markdown | Machine overview — OS, chip, memory, disk, load, top processes |
er-ports |
text | Listening TCP ports with process names |
er-services |
text | Running launchd user agents (non-Apple) |
er-brew |
markdown | Homebrew status — outdated packages, installed counts |
er-example |
markdown | Demonstrates all markdown rendering features |
Page scripts are regular shell scripts. They can output any supported format. Set scripts_dir in the config so scripts are on PATH automatically (important for launchd services which have a minimal PATH).
#!/bin/sh
# my-script — description
# Format: markdown
echo "# My Dashboard"
echo ""
echo "| Key | Value |"
echo "|-----|-------|"
echo "| Time | $(date) |"
echo "| User | $(whoami) |"Commands can only be registered in the config file — there is no API for adding commands at runtime. All output is HTML-escaped before rendering. See SECURITY.md for the full threat model.
Create eventrelay.yaml (see eventrelay.example.yaml):
# Server settings (flags override these)
server:
port: 6060
bind: 127.0.0.1
# token: mysecret
buffer: 1000
# log: /var/log/eventrelay/events.jsonl
notify:
- name: errors to slack
match:
level: error
slack:
webhook_url: https://hooks.slack.com/services/T00/B00/xxx
- name: deploys to discord
match:
source: ci
action: deploy
discord:
webhook_url: https://discord.com/api/webhooks/xxx/yyy
- name: forward to webhook
match:
source: myapp
webhook:
url: https://example.com/hooks
headers:
Authorization: Bearer mytokeneventrelay --config eventrelay.yamleventrelay --bind 0.0.0.0 --token mysecretWith --token, POST requests require Authorization: Bearer mysecret.
--port int listen port (default 6060)
--bind string bind address (default 127.0.0.1)
--token string require Bearer token for POST
--log string append events to JSONL file
--buffer int ring buffer size (default 1000)
--config string notification config file
--tui connect as TUI dashboard client
--url string server URL for TUI mode
--status check if eventrelay is running
--version print version and exit
make install # build and install binary to /usr/local/bin
make install-service # install + start on login via launchd
make status # check if running
make upgrade # build, replace binary, restart service
make restart-service # restart without rebuilding
make uninstall-service # stop and remove serviceAfter pulling new code, run make upgrade. This stops the running service, installs the new binary, and restarts via launchd. The service has KeepAlive enabled, so launchd handles the restart automatically if the process exits.
If you installed via go install without the launchd service, stop the running process (kill $(cat ~/.config/eventrelay/eventrelay.pid)), then go install github.com/dmoose/eventrelay@latest and start again.
eventrelay has real value as an intranet dashboard — a single URL for your team to see events, system status, and tool output. Do not expose it to the public internet.
The deploy/ directory contains a ready-to-use setup with Caddy for TLS and optional basic auth:
cd deploy
docker compose up -dThis gives you:
- eventrelay on port 6060 (internal)
- Caddy reverse proxy with automatic TLS on ports 80/443
- Basic auth (optional, see
deploy/Caddyfile)
Configure the domain in deploy/Caddyfile and event token in deploy/eventrelay.yaml.
SDKs/agents → eventrelay:6060 (Bearer token auth)
Browsers → Caddy (TLS + basic auth) → eventrelay:6060
- eventrelay handles SDK authentication via
--token - Caddy handles browser authentication via basic auth
- This separation means SDKs use token auth (no browser needed) while the dashboard is password-protected
See deploy/Caddyfile for examples including protecting only the dashboard while leaving the event API open.
eventrelay is designed for localhost and trusted networks. See SECURITY.md for the full threat model covering:
- On-device security (localhost default)
- Network deployment considerations
- Page command security model
- XSS prevention in the dashboard
- What NOT to do
See ARCHITECTURE.md for design details on the ring buffer, SSE fan-out, notification pipeline, and pages system.
MIT — see LICENSE.