Last updated 2026-05-28
What is notify
notify is a multi-channel notification platform packaged two ways:
- As a library —
import "github.com/elloloop/notify"and embed the orchestrator in-process. Inject aStoreand one or moreProviders; callNotifier.Notifywith a request. - As a container —
ghcr.io/elloloop/notify:<version>, a thin server wrapper that exposes the same orchestrator over gRPC (for backend producers) and Connect/HTTP-2 (for browser + mobile recipients).
Both packaging modes share the same internals: every type, function and test in the library is the exact code the container runs. Embedding the library is just choosing to skip the network hop.
Design goals
- One image, deployed once per environment. A pinned
image tag plus a small set of
NOTIFY_*env vars is the whole deployment surface. No bespoke deploy pipelines. - Pluggable everywhere it matters. Channels and their
providers are wired by configuration, not code. The store is the same
story: memory · Postgres · EntDB drivers all satisfy one
notify.Storeinterface and run the same conformance suite. - Domain-neutral. A
Notificationcarries an opaqueSubjectRef+SubjectType. The platform never interprets them — notodo_id,message_idor any other consumer-specific fields leak across the API. - Idempotent producer API. Every
Notifycall is keyed by(tenant_id, user_id, notification_id). Retries are safe and never duplicate stored rows or provider dispatches. - Per-recipient lifecycle. Delivery, ack, read timestamps are stamped per user copy. Fan-out to N users produces N rows with N independent lifecycles.
What it is not
- Not a template engine. The platform delivers the
Title+Body+Datathe caller hands it. Localization, MJML rendering and HTML email composition are the caller's concern (or a future wave). - Not an opinionated preferences system. Per-user
channel preferences ("Alice opted out of SMS") are explicitly out of
scope at v0.1; callers either skip channels themselves or store an
address of
""to keep the row but suppress dispatch. - Not an OAuth IdP. Authentication is done elsewhere (identity in the reference stack). notify validates the JWT it receives — it does not issue tokens.
- Not a polling endpoint masquerading as realtime. The
in-app channel uses a real server-streaming RPC (Connect/SSE). When
offline, in-app deliveries are
StatusPendingrather than a lyingStatusDelivered.
Surface area
Two services live in the proto contract (proto/notify/v1):
-
NotificationInternalService.Notify— the backend producer RPC. Fan a payload out to one-or-many users across one-or-many channels. -
NotificationClientService— the client-facing surface a browser SPA or mobile app calls. RPCs:StreamEvents,GetNotifications,AckNotification,AckDataChange,RegisterPushToken.
Both are accessible via Connect-Go / Connect-Web / Connect-Python / gRPC. There is no separate REST surface — Connect speaks JSON natively over HTTP/1 and HTTP/2.