Apache 2.0 · self-hostable

One API call. Email, SMS and an in-app inbox.

Hermes fans a single notification out across channels — with real-time delivery to the browser over WebSocket, per-user preferences, and templates you manage rather than hard-code.

Sending a notification with curl
curl -X POST https://hermes.example.com/v1/send \
  -H "Authorization: Bearer $API_KEY" \
  -d '{"to":{"user_id":"user123"},
     "template":"welcome","data":{"name":"Alice"}}'
Illustration: the Hermes inbox widget embedded in a product, showing live notifications
app.yourproduct.com
Inbox live
  • Deployment api-gateway promoted to production kargo · 4s ago
  • Alice invited you to Platform invites · 2m ago
  • Weekly usage report is ready digests · yesterday

<hermes-inbox api="…" token="…" socket="…">

Every product eventually grows a notification layer.

Transactional email, an SMS fallback, a bell icon with unread badges, a preferences page, and the plumbing to keep them consistent. Teams usually build it twice — once badly, once again under load — or hand it to a SaaS vendor along with their user data.

Hermes is that layer, built properly and run on your own infrastructure.

What it does

Multi-channel from one request

Email over SMTP or AWS SES, SMS via webhook to your gateway of choice, and an in-app inbox — all from a single send. Adding a channel is a configuration change, not an integration project.

Real-time in-app inbox

Notifications appear the moment they are delivered, pushed over a WebSocket via Centrifugo. No polling, no page refresh.

A widget you can drop in

Bell, badge, panel, live updates and mark-read actions ship as a standard custom element — any framework or none — with a native React binding over the same element.

Templates and preferences

Templates hold per-channel content and resolve at dispatch time with a Redis-backed cache. Categories carry default channels; a required category bypasses opt-in entirely.

Delivery you can audit

Every attempt emits an event. Status only advances — pending → sent → delivered → read → archived — and terminal failures land in a dead-letter stream for inspection and replay.

Self-hosted, Apache 2.0

No per-notification pricing, no vendor holding your recipient list. A Helm chart installs every service; a reference Terraform deployment runs it for real.

How it works

Nine Go services, NATS JetStream between each stage

  1. 01

    Send

    Auth, idempotency, publish. No DB on the hot path.

  2. 02

    Dispatch

    Persists, resolves templates and channels, fans out.

  3. 03

    Workers

    SMTP/SES, SMS webhook, inbox push via Centrifugo.

  4. 04

    Event writer

    Batch-inserts events, rolls status forward. Never backwards.

  5. 05

    Browser

    WebSocket → HTTP-streaming → polling, negotiated.

Write path — API key auth

Send is a deliberately thin ingestion layer: it authenticates the API key, applies idempotency, and publishes to NATS. Dispatch persists the notification record, resolves templates and channels, and fans out to per-channel delivery subjects.

Read path — JWT auth

Inbox Service serves the user's inbox with cursor-based pagination. User Service manages profiles and notification preferences. Centrifugo pushes live updates on user-scoped channels.

Two auth modes, kept separate

API keys, HMAC-SHA256 hashed at rest, for server-to-server Send and Admin APIs. Short-lived Hermes-issued JWTs, multi-key for rotation, for the user-facing Inbox and User APIs.

Services
Nine Go services — send, admin, dispatch, event writer, three delivery workers, inbox, user
Messaging
NATS JetStream — work-queue streams for notifications, delivery and events, plus a dead-letter stream
Database
PostgreSQL, shared, migrated with golang-migrate
Cache
Redis / Valkey — template cache, idempotency dedup, Centrifugo engine
Realtime
Centrifugo, user-scoped channels
Frontend
Next.js admin portal; custom-element + React inbox widget
Observability
OpenTelemetry to an in-cluster LGTM stack (Loki, Grafana, Tempo, Prometheus)

Self-hosted, or hosted for you

Where the two models actually differ

Hermes, self-hosted Hosted notification SaaS
Cost model Your infrastructure bill. No per-notification pricing. Priced per notification or per monthly active user.
Recipient data Stays in your PostgreSQL, in your VPC. Contact points and send history held by the vendor.
Source Apache 2.0. Fork it, audit it, patch it. Closed; behaviour is what the docs say it is.
Delivery evidence Every event in a table you can query, with a dead-letter stream. Whatever the dashboard and export API expose.
Operational burden Yours: Kubernetes, Postgres, NATS, Redis. Terraform and Helm provided. Theirs. This is the real trade.
Support Community. Developed non-commercially, supplied free of charge. Contractual, with an SLA.

Get started

Evaluate in five minutes.

A Helm chart installs every service plus bundled PostgreSQL, NATS, Redis and Centrifugo, with the schema migration and stream provisioning applied automatically.

Run it for real

Terraform for VPC, EKS, Aurora PostgreSQL, ElastiCache and ECR — all on Graviton — with ArgoCD for GitOps sync and Kargo for staged promotion behind an operator approval.

Develop on it

make dev-up brings up a full local Kubernetes environment on k3d with Tilt and hot reload. Unit, integration, end-to-end and k6 load tests ship with it.

install
Installing Hermes with Helm
helm install hermes oci://ghcr.io/hermesnotifications/charts/hermes \
  --namespace hermes --create-namespace \
  --set global.domain=hermes.example.com \
  --set hermes.jwt.secret="$(openssl rand -base64 32)" \
  --set hermes.apiKey.hmacSecret="$(openssl rand -base64 32)"

The bundled datastores are for evaluation only; production means external datastores over TLS and the documented hardening steps.

For developers

Docs hub · API reference · integration guide · ADRs

SDKs in four languages

Server SDKs, plus browser packages for the client, the React binding and the embeddable widget.

  • @hermes/node
  • hermes-python
  • hermes-java
  • Hermes.NET
  • @hermes/react
  • @hermes/inbox-widget

An admin portal

A Next.js UI for organizations, categories, templates and API keys.

slugchannelsupdated
welcomeemail · inbox2d ago
password-resetemail · sms5d ago
deploy-promotedinbox1w ago

A CLI, including a terminal inbox

hermes manages categories, templates and API keys, sends notifications, and opens an interactive TUI inbox with live updates.

The hermes CLI showing a terminal inbox and a send command
$ hermes inbox --user user123
┌─ Inbox ──────────────────── ● live ─┐
  api-gateway promoted      4s │
  Alice invited you         2m │
│   Weekly usage report      1d │
└──── r read · a archive · q quit ────┘

$ hermes send --template welcome --to user123
queued  01J8F2… → email, inbox