Why notifkit
notifkit is a self-hosted notification engine. Your app makes one call; notifkit decides who to reach, on which channel, with what content and when, and retries until the message lands or provably fails.
The problem it solves
Sending one notification is easy: take a Resend or Twilio API key and call
send(). Making that call correct is the hard part.
One user opted out of SMS last week. Another is asleep in Tokyo. A third has three devices, two
with dead push tokens. A worker crashed mid-batch and you can't tell which half went out.
Marketing wants a reminder three days after signup, unless the invite was accepted. Each is a
small change; together they turn send() into a distributed state machine you never
planned to build.
notifkit is that state machine. You decide what to say; it handles getting it there.
What a send actually goes through
Every request passes a fixed sequence of gates before it reaches a provider. Each gate can drop, defer or pass the notification:
notification:skipped event with
a reason. The amber exit is a deferral: 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 unsubscribes, complaints and dead addresses.
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, slack, whatsapp, telegram, discord, 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. A request's x-idempotency-key is honoured for 24 hours. |
| 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, Twilio, Slack, WhatsApp, Telegram and Discord, plus 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
You run it. There's no hosted version; you need your own PostgreSQL database and Redis instance.
A campaign is a label on a send, for reporting afterwards. There's no campaign builder, visual editor, A/B testing or audience segmentation UI. If you need marketing automation, this is the wrong tool: it's infrastructure for product notifications that reports honestly on what it delivered.
notifkit delivers nothing itself. It drives Resend, FCM, Twilio or whatever you plug in.
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. Exactly-once isn't possible across a remote provider API without two-phase commit, so tools that advertise it are describing their own deduplication, not what reached the user. notifkit deduplicates at four stages too, but doesn't call that a guarantee. See delivery guarantees.