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:
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
| Concern | How 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.