← website
Start here · 01

Why NotifKit

NotifKit is a self-hosted notification engine you run inside your own infrastructure. Your app makes one call. NotifKit decides who to reach, on which channel, using which content, at what time — and keeps trying until it either lands or provably fails.

The problem it solves

Sending one notification is easy. You take an API key from Resend or Twilio and call send(). The trouble starts the moment that call has to be correct.

A user opted out of SMS last week. Another is asleep in Tokyo. A third has three devices and two of the push tokens are dead. Your worker crashed halfway through a batch and you cannot tell which half went out. Marketing wants a reminder three days after signup, but only if the user has not accepted the invite. Every one of these is a small change, and together they turn send() into a distributed state machine that you did not plan to build.

NotifKit is that state machine, already built. You keep the decision of what to say; it takes the mechanics of getting it there.

What a send actually goes through

A notification is not forwarded — it is evaluated. Each request passes a fixed sequence of gates before any provider is called, and each gate can drop, defer, or pass it along:

dedupe duplicate opt-out topic off channel channel off quiet hrs deferred template not found contact no address suppressed opted out throttle too many dispatch → provider notify() delivered
The gate sequence. Red exits are terminal for that channel and emit a notification:skipped event with a reason. The amber exit is not a failure — a quiet-hours hit is rescheduled to the end of the window, not dropped. priority: "critical" bypasses quiet hours and the throttle — but never the suppression gate, which records that someone unsubscribed, complained, or that the address is dead.

What NotifKit gives you

ConcernHow NotifKit handles it
Targeting Send to a user id, an inline user object, an array of users, a segment, or a topic. Fan-out happens server-side.
Preferences Per-channel and per-topic opt-outs plus quiet-hours windows live on the user record and are enforced before rendering.
Consent One-click unsubscribe headers on topic-bearing mail. Unsubscribes, spam complaints, and hard bounces suppress the address on every later send.
Reporting Tag a send with a campaign label and read back delivered, opened, clicked, bounced, complained, and unsubscribed — with rates.
Channels email, sms, push, webhook. Send to several at once, or chain them as an ordered fallback.
Durability Redis Streams with consumer groups. A worker that dies mid-job leaves the message unacked; another worker reclaims it.
Deduplication Idempotency guards at the enricher, engine, and delivery stages, with a 24-hour window.
Scheduling sendAt for future delivery, cancellable until dispatch. Quiet-hours deferral uses the same machinery.
Workflows Multi-step sequences that can wait("3d") or waitForEvent("invite.accepted") and survive process restarts.
Providers A Transport interface. First-party packages for Resend, FCM, and a console transport for local development.
Multi-tenancy Every row is scoped to a project. API keys are project-scoped and hashed at rest.
Observability Prometheus metrics, structured logs, per-message delivery history in Postgres, and a dead-letter queue you can inspect and replay.

What it is not

  • Not a hosted service. You run it. It needs a PostgreSQL database and a Redis instance you control.
  • Not a marketing automation suite. A campaign here is a label you attach to a send so you can report on it afterwards — there is no campaign builder, no visual editor, no A/B testing, and no audience segmentation UI. It is infrastructure for product notifications that happens to be honest about what it delivered.
  • Not an email provider. It does not deliver anything itself — it drives Resend, FCM, Twilio, or whatever you plug in.
  • Not exactly-once. Delivery is at-least-once. If a provider accepts a message but the response times out, NotifKit retries and the user may see it twice. Nothing else can honestly promise otherwise — exactly-once is unachievable across a remote provider API without a two-phase commit, so a tool that advertises it is describing its own deduplication, not what reached the user. NotifKit deduplicates at four stages too; it just does not call that a guarantee. See delivery guarantees.

Where to go next