AlertLoop is a self-hosted event, incident, and business notification center for software teams and operations-heavy businesses.
It helps applications send important technical incidents and business events into one reliable place, track their status, and deliver them through channels such as email, Telegram, and webhooks. Planned paid editions will add channels such as WhatsApp.
AlertLoop starts as a self-hosted product. Cloud SaaS is intentionally deferred until the core product and market positioning are validated.
- Community (this repository): free, self-hosted, backend API, Swagger/OpenAPI, basic events list page, Email/Telegram/Webhook delivery, delivery retries and dead-letter replay, SQLite and PostgreSQL.
- Pro Self-hosted (planned, paid): multi-project, routing rules, SDKs, WhatsApp, Telegram fallback/proxy, RBAC, retention policies (per-project and per-event-type rules managed from the UI), escalation policies.
- Enterprise (planned, paid): on-prem license, SSO, HA, audit, custom adapters, support.
- Events API with API-key auth and OpenAPI/Swagger.
- Three event families:
incident,business_event,audit. - Event lifecycle:
new,acknowledged,resolved,muted,escalated(with a manualescalateaction). - Idempotent ingestion via
dedupe_key. - Delivery channels: Email (SMTP), Telegram, Webhook (HMAC-signed).
- Delivery worker with retries, exponential backoff, dead-letter, and replay.
- Delivery attempt history, separate from event state.
- Three simple built-in web pages protected by an admin token: events list
(
/events), event detail (/events/{id}), and deliveries (/deliveries) — next to the richer React admin console at/admin. - Cursor-paginated list endpoints.
- Structured logs (text or JSON), to stdout or a file.
- Event retention with automatic cleanup (30 days by default, configurable via
retention_days).
- Go: 1.25 or newer — only to build from source. Release binaries are static and need no runtime; there is no CGO dependency.
- PostgreSQL (production): 12 or newer (14+ recommended, tested on 16). Uses
JSONB, partial indexes, andFOR UPDATE SKIP LOCKED. - SQLite (local/demo): none — it is embedded (pure-Go driver). Just a file.
- Docker (optional): any recent Docker Engine with Compose v2 for the container deployment path.
docker compose up -dThen open:
- API docs: http://localhost:8080/swagger
- Events page: http://localhost:8080/events?token=change-me-admin (admin token)
Download a release binary (linux/amd64, linux/arm64; darwin/windows for local evaluation), then run all-in-one mode backed by SQLite:
./alertloop --config alertloop.yaml allTo install as a systemd service, see deploy/systemd/ (install.sh).
make run # build + run all-in-one on :8080 (SQLite)
make test # run the test suiteThe API accepts your admin token (full access) or a scoped API key. For a quick test, use the admin token from your config:
curl -X POST http://localhost:8080/v1/events \
-H "X-API-Key: change-me-admin" \
-H "Content-Type: application/json" \
-d '{
"type": "incident",
"severity": "critical",
"source": "feeds_worker",
"message": "Feed processing failed",
"dedupe_key": "feeds:developer:15:flats"
}'For real integrations, create least-privilege API keys with a scope
(ingest for event sources, read for dashboards, full for trusted tools) in
the config file — see alertloop.example.yaml.
The single alertloop binary runs in three modes:
server— HTTP API and web UI.worker— delivery workers and retention cleanup.all— both in one process (default; ideal for small installs).
AlertLoop ships a React admin console (Overview, Events, Event detail,
Deliveries with dead-letter replay, About). It is embedded in the binary and
served at /admin on the same origin as the API — nothing extra to install or
run. With either deployment path you simply open /admin — but where it is
reachable from differs, because the two paths bind the port differently:
- Local (either path): http://localhost:8080/admin.
- Binary / systemd on a server: AlertLoop listens on
:8080on all interfaces, so it is also reachable athttp://<server-ip>:8080/admin. - Docker Compose on a server: both bundled Compose files publish the port on
127.0.0.1only, sohttp://<server-ip>:8080/adminis refused by design — a published Docker port is not filtered by a host firewall such as ufw, so binding0.0.0.0would silently expose the console. Reach it through the reverse proxy indeploy/proxy/, through an SSH tunnel (ssh -L 8080:127.0.0.1:8080 user@server, then use the Local URL above), or by deliberately changing the mapping to"8080:8080"if you accept the exposure.
For a real domain with HTTPS (the recommended setup for either path), see Access over a domain.
Sign in with the admin token from your config (there are no user accounts in Community). The token is kept in the browser's session storage and sent to the API. Because the console is same-origin with the API, it works at any host or domain with no rebuild and no CORS configuration.
The console has a light responsive layout: on phones the sidebar collapses into a menu and wide tables scroll horizontally. A full mobile experience is not a Community goal — use a desktop browser for day-to-day work.
cd web/admin
npm install
npm run dev # http://localhost:5273, proxies /v1 to localhost:8080Build it into the binary with make admin (outputs to internal/adminui/dist,
which the Go build embeds). The Docker image build does this automatically, so
go build and docker build both produce a binary/image with the console
already inside.
Where AlertLoop is reachable before you add a proxy depends on how you run it:
- Binary / systemd: the process listens on
:8080on all interfaces, so on a server it is reachable athttp://<server-ip>:8080(restrict it with your host firewall, or bind it to loopback withaddr: "127.0.0.1:8080"). - Docker Compose: both bundled Compose files publish the port as
127.0.0.1:8080:8080, so the container is reachable from the host only. That is deliberate: a published Docker port is not filtered by ufw/firewalld, so binding0.0.0.0would put the admin console on the public internet without the host firewall noticing.
Either way, for a real domain put an HTTPS reverse proxy in front — AlertLoop itself speaks plain HTTP and does not terminate TLS. Since the API, admin console, and Swagger are all one origin, a single proxy rule covers everything:
[browser] --HTTPS--> [nginx/Apache :443] --HTTP--> [alertloop 127.0.0.1:8080]
Then everything is on your domain, same-origin, no CORS:
https://alerts.example.com/admin— admin consolehttps://alerts.example.com/v1/...— APIhttps://alerts.example.com/swagger— API docs
Ready-to-adapt configs are in deploy/proxy/:
- nginx —
deploy/proxy/nginx.conf - Apache —
deploy/proxy/apache.conf
Both redirect HTTP→HTTPS and forward to 127.0.0.1:8080 — which is exactly what
the Compose port mapping publishes, so the proxy path needs no Compose changes.
Get a certificate with Let's Encrypt (certbot). HTTPS is required in
production — the admin token travels with each request and must not go over
plaintext HTTP.
Point the DSN at a file path; AlertLoop creates the file and its tables
automatically on first run. :memory: is supported for throwaway runs.
database:
driver: sqlite
dsn: alertloop.dbAlertLoop connects to an existing database and creates/updates only its own
tables (events, delivery_attempts, schema_migrations) via migrations that
run automatically at startup. It does not create the database itself — the
target database must already exist (this is standard PostgreSQL client behavior:
you cannot connect to a database that has not been created).
Create the database once:
createdb -U postgres alertloop
# or: psql -U postgres -c "CREATE DATABASE alertloop;"Then point AlertLoop at it. With the binary, put it in your config file
(e.g. alertloop.yaml) — this is the only part you change to switch from SQLite
to PostgreSQL:
database:
driver: postgres
dsn: "postgres://USER:PASSWORD@HOST:5432/alertloop?sslmode=require"./alertloop --config alertloop.yaml all(Or via environment variables instead of the file:
ALERTLOOP_DB_DRIVER=postgres ALERTLOOP_DB_DSN="postgres://…" ./alertloop all.)
Use sslmode=require for a remote database; sslmode=disable is only for a
local/private-network Postgres.
Using an existing/shared database is fine: AlertLoop only touches its own
tables and never drops or alters unrelated tables. Just make sure no other
application uses tables named events, delivery_attempts, or
schema_migrations in that database. (A configurable table prefix/schema is on
the roadmap for shared databases.)
AlertLoop writes structured logs. By default they go to stdout; set a file path to also persist them for external tools.
log:
level: "info" # debug | info | warn | error
format: "text" # text (human-readable) | json (for Loki/Elasticsearch/Vector)
file: "" # empty = stdout; e.g. /var/log/alertloop/alertloop.logHow to read logs by deployment:
- Binary (stdout): run in a terminal, or redirect:
./alertloop all >> alertloop.log 2>&1. - Binary (file): set
log.file(orALERTLOOP_LOG_FILE) and read it with any tool:tail -f /var/log/alertloop/alertloop.log. - systemd: logs go to the journal —
journalctl -u alertloop -f. Setformat: jsonfor machine-readable output that log shippers can parse. - Docker:
docker compose logs -f alertloop(ordocker logs).
For centralized logging, set format: json and point your log collector at the
file or the container's stdout.
Precedence (highest wins): command flags → environment variables → YAML config
file → built-in defaults. See alertloop.example.yaml and .env.example for the
full list.
You can run AlertLoop with no channels configured — it will accept and store
events (visible in the API and at /admin) and simply deliver nothing. This is
the default in the example configs, so the app starts out of the box. To send
notifications, configure one or more channels (email/telegram/webhook) in the
YAML file. If you enable a channel you must fill in all of its required fields,
or startup fails with a clear message (this catches typos like a missing
bot_token).
Every event is delivered to every configured channel — Community has no per-event routing (that is a paid capability).
The channel configuration is used by both roles: the server decides which
delivery jobs to create when an event arrives, and the worker performs the
actual sending. Both processes must load the same channel configuration — if
the server has no channels, no delivery jobs are created and nothing is ever
sent (it will log a prominent warning at startup).
The PostgreSQL Compose profile (deploy/docker/docker-compose.postgres.yml)
does this correctly by mounting a single alertloop.yaml into both the api
and worker containers — one source of truth, no drift. To use it:
cd deploy/docker
cp alertloop.yaml.example alertloop.yaml # set admin_token; api_keys & channels optional
docker compose -f docker-compose.postgres.yml up -dThe single-process all mode (and the SQLite demo) has no such split and needs
no special handling.
The OpenAPI contract lives at api/openapi.yaml and is served live at
/openapi.yaml, with Swagger UI at /swagger.
Documentation lives in two places today: this README (installation, deployment,
configuration, operations) and the interactive Swagger UI at /swagger on a
running instance (the full endpoint reference, request/response schemas, and
examples).
Download a prebuilt binary for your OS/arch from the repository's GitHub
Releases page — pick linux_amd64, linux_arm64, darwin_arm64,
windows_amd64, etc., unpack, and run. Each release also ships a
checksums_*.txt to verify the download.
You do not need a Windows machine to get a Windows binary (or a Mac for a Mac
binary). AlertLoop is CGO-free (pure-Go SQLite), so it cross-compiles — a
single build machine produces every OS/arch at once. Releases are built
automatically by CI when a version tag is pushed (.github/workflows/release.yml),
which builds the admin UI, cross-compiles all targets, and uploads them to the
GitHub Release.
To build the release artifacts yourself:
make admin # build the embedded admin UI (needs Node)
make release # cross-compile all targets into dist/
# equivalently: VERSION=v0.1.0 ./scripts/build-release.shFor a single local binary for your own machine, just make build (Go only).
- Always set
admin_token: if neitheradmin_tokennorapi_keysare set, the API accepts unauthenticated requests with full scope (a local-demo convenience; the process logs a warning at startup). Always setadmin_tokenon anything reachable by others. - TLS: AlertLoop serves plain HTTP and is designed to run behind an HTTPS reverse proxy (nginx, Caddy, Traefik). Terminate TLS there and forward to the container/port.
- External PostgreSQL: use
sslmode=require(or stricter) in the DSN. The bundled Compose Postgres usessslmode=disableonly because it is on a private Docker network. - Rate limiting is on by default (
rate_limitin the config). If your proxy already rate limits, you can disable it here. - SMTP: set
starttls: true(required upgrade, port 587) ortls: true(implicit TLS / SMTPS, port 465). AlertLoop will not send mail in plaintext when TLS is requested but unavailable. - Secrets: never commit real
alertloop.yaml/.env(they are gitignored); keep the*.exampletemplates only.
Community Edition is licensed under AGPL-3.0-only (see LICENSE and
NOTICE). Planned Pro and Enterprise editions will be offered under a separate
commercial license.