Last updated 2026-05-28

What is notify

notify is a multi-channel notification platform packaged two ways:

  • As a libraryimport "github.com/elloloop/notify" and embed the orchestrator in-process. Inject a Store and one or more Providers; call Notifier.Notify with a request.
  • As a containerghcr.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.Store interface and run the same conformance suite.
  • Domain-neutral. A Notification carries an opaque SubjectRef + SubjectType. The platform never interprets them — no todo_id, message_id or any other consumer-specific fields leak across the API.
  • Idempotent producer API. Every Notify call 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 + Data the 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 StatusPending rather than a lying StatusDelivered.

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.