Self-hosted notification infrastructure for product notifications. One API call handles email, SMS, push, and webhooks, with preferences, quiet hours, retries, fallback, scheduling, workflows, and delivery logs built in.
110
stars
56
commits
TypeScript
primary language
Sep 8, 2026
updated
You shouldn't have to build a notification system.
Self-hosted notification infrastructure for product notifications. One API call handles email, SMS, push, and webhooks, with preferences, quiet hours, retries, fallback, scheduling, workflows, and delivery logs built in.
await sendEmail({
to: user.email,
subject: "Order Shipped",
...
});
Sending them reliably is the hard part. Users opt out. People are asleep. Push tokens die. Providers throw 503s. Channels fail and need fallback. Somewhere along the way you need timezone-aware quiet hours, future scheduling, deduplication, multi-channel templates, delivery logs, unsubscribe handling, multi-step workflows, and a dead-letter queue nobody wants to maintain.
notifkit is that machinery. Your app makes one typed call, and notifkit decides who gets the notification, which channel to use, when to send it, whether the user is allowed to receive it, and what to do when delivery fails.
import { notifkit } from "notifkit";
await notifkit.notify({
user: "usr_123",
template: "order-shipped",
channels: ["push", "email"],
fallback: true,
});
That call tries push, then email if push fails. Preferences, consent, quiet hours, retries, deduplication, throttling, template rendering, and delivery tracking all happen behind it.
notifkit is an orchestration engine and a typed SDK.
flowchart TD
App["Your Application / AI Agent<br/>Typed SDK · REST API · MCP Server"]
App -->|"HTTP POST /v1/notify"| API
API["Notifkit API Server<br/>Schema Validation · Auth · Multi-Tenancy<br/>Idempotency Gate · Priority Queue Ingestion"]
API --> PG
API --> REDIS
PG[("PostgreSQL — Storage<br/>Users · Preferences<br/>Templates · Workflows<br/>Delivery Logs · DLQ")]
REDIS[("Redis — Streams / ZSET<br/>Priority Queues<br/>Scheduled Sends<br/>Sliding Rate Limits")]
subgraph WORKERS["Background Workers Pipeline"]
direction LR
ENRICH["Enricher<br/>(Resolve)"] --> ENGINE["Engine<br/>(Quiet Hours)"] --> DELIVER["Delivery<br/>(Rate Limits / CB)"]
ENGINE --> SCHED["Scheduler<br/>(sendAt / QH)"]
SCHED --> DELIVER
end
REDIS -->|"consume"| ENRICH
ENRICH -.->|"read / write state"| PG
DELIVER -.->|"delivery logs"| PG
DELIVER -->|"Dispatch"| PROVIDERS
PROVIDERS["Provider Transports<br/>Email: Resend · Push: Firebase (FCM) · SMS: Twilio<br/>Chat: Slack, Telegram, Discord, WhatsApp<br/>Webhooks: Custom HTTP"]
classDef entry stroke:#6366f1,stroke-width:2px
classDef store stroke:#0ea5e9,stroke-width:2px
classDef work stroke:#22c55e,stroke-width:2px
class App,API,PROVIDERS entry
class PG,REDIS store
class ENRICH,ENGINE,DELIVER,SCHED work
NotifkitServer runs the HTTP REST API router (/v1/notify, /health, /metrics) and the background worker pipelines: enricher, decision engine, scheduler, and delivery. NotifkitClient is the lightweight client your application uses to trigger notifications, sync templates, and manage users over HTTP.
In a single process, the API and all workers run in the same Node.js process (services: ["all"]), which works for small and medium apps, side projects, and staging. Distributed, you run stateless API servers (services: ["api"]) behind a load balancer and scale worker pools (services: ["enricher", "engine", "delivery", "scheduler"]) horizontally across Redis Streams consumer groups.
npm install notifkit @notifkit/provider-resend
npm install -D tsx @testcontainers/postgresql @testcontainers/redis
The two @testcontainers/* packages are what notifkit uses to start throwaway PostgreSQL and Redis containers in development. They are imported lazily, only when the server is given neither a databaseUrl/redisUrl option nor a DATABASE_URL/REDIS_URL environment variable, so devDependencies is the right place for them.
[!WARNING] Those containers are for local development only. They are thrown away when the process exits, taking every user, template, delivery log, and queued notification with them. Before you deploy, point notifkit at a real PostgreSQL and Redis — set
DATABASE_URLandREDIS_URL(or passdatabaseUrlandredisUrl) and run withNODE_ENV=production, which refuses to start a container and fails loudly if either is missing.
server.ts starts the API and the worker pipelines. In development it auto-starts those containers, so Docker is the only prerequisite.
// server.ts
import { NotifkitServer } from "notifkit";
import { ResendTransport } from "@notifkit/provider-resend";
const server = new NotifkitServer({
services: ["all"], // API + enricher + engine + scheduler + delivery
port: 3000,
providers: [
new ResendTransport({
apiKey: process.env.RESEND_API_KEY!,
from: "notifications@yourdomain.com",
}),
],
});
await server.start();
console.log("notifkit listening on http://localhost:3000");
from is required on ResendTransport — it is the sender for any template that does not name its own, and it has to be an address on a domain you have verified in Resend. A template can override it with its own from, so one transport can serve both no-reply@ receipts and marketing@ campaigns.
ADMIN_API_KEY is the root credential. It is read from the environment, it is what mints project API keys in the next step, and without it the project-management routes answer 403. Any string works locally:
ADMIN_API_KEY=supersecretkey RESEND_API_KEY=re_xxx npx tsx server.ts
[!WARNING]
supersecretkeyis a local placeholder. In production this one value can mint keys for every project, so use a long random string kept in your secret store —openssl rand -hex 32is enough.
Every /v1/* route requires a project API key, and only the admin credential can mint one, so this is the single bootstrap step between a running server and your first notification:
ADMIN_API_KEY=supersecretkey npx notifkit-create-project "my-app"
Project "my-app" created. Save the API key now — it is not recoverable.
NOTIFKIT_PROJECT_ID=1ce67fa1-b4a9-4985-8046-ef6018912b2a
NOTIFKIT_API_KEY=nk_live_f57c57b76d795cef89e2dbf6b6f352a36…
The server stores only a SHA-256 hash of the key, so the nk_live_… value is printed once and never again — put it in your app's .env now. Point the script at another host with NOTIFKIT_URL, and mint further keys later with POST /v1/projects/:id/keys (role: "read_only" there gets you a key that can read but not send).
client.ts is your application code. It talks to the server over HTTP: register a template, register a user, and send.
// client.ts
import { NotifkitClient } from "notifkit";
const notifkit = new NotifkitClient({
baseUrl: "http://localhost:3000",
apiKey: process.env.NOTIFKIT_API_KEY!,
});
// 1. Register a template
await notifkit.syncTemplates({
templates: [
{
id: "order-shipped",
channel: "email",
content: { subject: "Order #{{orderId}} Shipped", text: "Your order is on the way!" },
},
],
});
// 2. Register a user
await notifkit.addUser({ id: "usr_123", email: "alex@acme.com" });
// 3. Dispatch
await notifkit.notify({
user: "usr_123",
template: "order-shipped",
channels: ["email"],
data: { orderId: "9481" },
});
With the server still running in the first terminal, run the client in a second one:
NOTIFKIT_API_KEY=nk_live_xxx npx tsx client.ts
The Node.js SDK is optional. notifkit exposes a standard HTTP REST API, so you can dispatch notifications and manage resources from any language (cURL, Python, Go, and so on). The same project API key goes in the Authorization header (an x-api-key header works too):
curl -X POST http://localhost:3000/v1/notify \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $NOTIFKIT_API_KEY" \
-d '{
"user": "usr_123",
"template": "order-shipped",
"channels": ["email"],
"data": { "orderId": "9481" }
}'
Locally, Docker is the only prerequisite: in development notifkit starts throwaway PostgreSQL and Redis containers for you. In production you need Node 22+, PostgreSQL, and Redis, and you run migrations by pointing drizzle-kit at node_modules/notifkit/drizzle.
notifkit runs in production at my own company, delivering 100K+ notifications a day across email, push, and OTPs. I built it because I needed it and didn't want to spend months rebuilding distributed notification plumbing or pay a SaaS per alert. It runs on your servers, with your provider accounts and your data.
Every component of the pipeline is tested against failure:
flowchart LR
S1["Redis Streams"] -->|"Kill Worker (SIGKILL)"| M1["Auto-Claim and Replay"] --> O1["Zero Lost Messages"]
S2["Connection Loss"] -->|"Drop DB / Redis"| M2["Auto-Reconnect / Retry"] --> O2["In-Flight State Intact"]
S3["10k+ Messages"] -->|"Burst"| M3["Concurrency and Limits"] --> O3["Flat Memory, No Leaks"]
classDef fault stroke:#ef4444,stroke-width:2px
classDef guard stroke:#6366f1,stroke-width:2px
classDef result stroke:#22c55e,stroke-width:2px
class S1,S2,S3 fault
class M1,M2,M3 guard
class O1,O2,O3 result
tests/chaos/crash.test.ts): background worker processes are killed with SIGKILL during high-throughput message streaming. Consumer group Pending Entries List (PEL) re-claims mean no messages are lost and another worker takes over.tests/chaos/recovery.test.ts): PostgreSQL and Redis connections are severed and restored under live traffic, verifying client reconnection, worker backpressure, and durable state resumption.tests/chaos/load.test.ts): bursts of 10,000+ notifications across parallel worker pools, checking queue drain speed, sliding-window rate limiters, and memory use over time.tests/race-conditions.test.ts, tests/idempotency.test.ts): concurrent duplicate dispatches, overlapping quiet-hour boundary evaluations, atomic user updates, and 24-hour idempotency key deduplication.| The problem you don't want to build | How notifkit solves it |
|---|---|
| "Should this user receive it?" | User preferences, topic opt-outs, and consent gates |
| "Is this a bad time to send?" | Timezone-aware quiet hours that defer non-urgent sends |
| "What if push fails?" | Ordered multi-channel fallback (push, then email, then sms) |
| "What if my worker crashes?" | Redis Streams consumer groups, retries, and durable idempotency |
| "What if an event fires twice?" | 24-hour deduplication via idempotency keys |
| "Can I send this later?" | Priority scheduling with sendAt and cancellation before dispatch |
| "Can I send this 3 days after signup?" | Stateful multi-step workflows with wait and waitForEvent |
| "How do I know what happened?" | Queryable delivery logs, Prometheus metrics, and campaign reporting |
| "What happens when a provider goes down?" | Circuit breakers, exponential backoff, and DLQ replay |
| "What about bounces and spam complaints?" | RFC 8058 one-click unsubscribe and automatic hard-bounce suppression |
| "What if I don't want another SaaS holding my data?" | Fully self-hosted on your PostgreSQL and Redis |
You decide what to say. notifkit gets it there.
notifkit is the durable notification layer that runs inside your own stack. It is not a marketing automation suite, and it does not replace Customer.io, OneSignal, or SendGrid. You bring your own provider accounts and pay them directly.
First-party providers cover Resend, Firebase Cloud Messaging, Slack, Twilio, Telegram, Discord, and WhatsApp. Anything else is a Transport class with a send() method.
Novu is the established open-source project in this space, and if you want a notification platform with a dashboard, a visual workflow editor, and a drop-in in-app inbox component, use Novu. It is more mature, has a much larger community, and solves a broader problem.
notifkit is a narrower, more embeddable take on the same layer:
What notifkit does not have: an in-app notification center or inbox component, a web dashboard for non-engineers, digest aggregation, or Novu's provider catalog. If you need those, Novu is the better fit.
https://github.com/user-attachments/assets/4dff98bb-37d3-44b4-bf46-9607c1cd89b5
An AI agent can operate notifkit directly. Connect the notifkit MCP server (@notifkit/mcp) to Claude Code, Cursor, Claude Desktop, Gemini, or any MCP-compatible agent:
npx -y @notifkit/mcp
You: Why didn't usr_9182 receive their password reset?
Agent: The notification was suppressed because usr_9182's email
address has a hard-bounce suppression from yesterday.
Your application and your AI agents use the same notification infrastructure. Through MCP an agent can:
send_notification, send_campaign)get_delivery_logs, get_notification)list_scheduled, cancel_notification)list_campaigns, get_campaign_stats)list_templates, preview_template, upsert_template)list_users, get_user_preferences, update_user_preferences)create_workflow, trigger_workflow, get_workflow_run)list_suppressions, get_dead_letters, replay_dead_letter)| Without an agent | With the notifkit MCP server |
|---|---|
| Query the database for contact info, open Twilio or Resend or write a throwaway script, format the payload, check the user's timezone by hand, send it, and hope it delivered. | You: "Send an urgent update to alex@acme.com that his package was lost in transit and support is rushing a replacement. Text him if push doesn't deliver." Agent: Looks up alex@acme.com, renders the template, dispatches push with SMS fallback, bypasses quiet hours because the send is urgent, tracks delivery status, and confirms it reached his phone. |
Already have notification code scattered across your application? Point your coding agent at:
https://notifkit.dev/llms-full.txt
It can read notifkit's API from there, find ad-hoc notification code in your repository, and refactor it into notifkit calls.
| Channels | email, sms, push, webhook, telegram, discord, whatsapp, slack |
| Targeting | A user, a list of users, a segment, or a topic |
| Priorities | low, normal, high, critical, on separate stream lanes |
| Scheduling | Future sends with sendAt, quiet-hours deferral, cancellation |
| Preferences | Per-user channel and topic opt-outs, quiet hours, contact-level overrides |
| Workflows | Multi-step sequences with wait, waitForEvent, and notify steps |
| Reliability | Redis Streams, 24h idempotency, retries, DLQ, provider circuit breakers |
| Templates | {{var}} interpolation with destination-aware escaping |
| AI | Optional LLM augmentation before render via the Vercel AI SDK |
| Multi-tenancy | Projects with isolated keys, data, and rate limits |
| Consent | RFC 8058 one-click unsubscribe; complaints and hard bounces suppress automatically |
| Reporting | Campaign labels with delivery and engagement totals |
| Agent operation | MCP server for sending, triage, campaigns, templates, workflows, and system operations |
| Observability | Prometheus /metrics, /health, /live, /ready, and queryable delivery logs |
Bring your own provider accounts. First-party packages:
@notifkit/provider-resend: transactional email via Resend@notifkit/provider-fcm: push notifications via Firebase Cloud Messaging@notifkit/provider-slack: Slack messages via Incoming Webhooks or the Web API@notifkit/provider-twilio: SMS via Twilio, with signature-verified delivery status callbacks@notifkit/provider-telegram: messages via a Telegram bot@notifkit/provider-discord: messages via a Discord webhook@notifkit/provider-whatsapp: messages via Meta's WhatsApp Cloud APIFor anything else, implement a Transport:
class MyTransport implements Transport {
async send(message) {
// Send through SES, Postmark, APNs,
// SendGrid, a custom webhook, or anything else.
}
}
The keys, the billing, and the deliverability stay yours.
Everything lives at notifkit.dev/docs.
| Quickstart | Install to first delivered notification |
| How it works | Core concepts and the notification pipeline |
| Channels & fallback | Multicast, ordered fallback, and custom transports |
| Preferences & quiet hours | Preference, consent, and timing rules |
| Templates & AI | Interpolation, escaping, and per-channel content |
| Segments & scheduling | Fan-out, priority lanes, sendAt, and idempotency |
| Workflows | Multi-step sequences, recurring sends, and digests |
| Examples | Runnable projects |
| Architecture | Streams, delivery guarantees, topologies, and data model |
| Deployment | Docker, Compose, and production topologies |
| Operations | Health, metrics, DLQ, key rotation, and shutdown |
| Reference | API, payloads, and SDK methods |
| MCP server | Operate notifkit from an AI agent |
Notification infrastructure looks simple until you're responsible for it. Queues, retries, provider adapters, preference systems, quiet-hour logic, workflows, suppression handling, and operational tooling take months to build well. notifkit is what I built instead, and it's what I run.
Issues and pull requests are welcome. Stars help other people find the project.
npm install
npm run build
npm test
The test suite starts its own PostgreSQL and Redis containers, so Docker is the only thing you need running.
Questions, bugs, or ideas: contact.devkitshq@gmail.com, or open an issue.
MIT. Do what you like with it, including commercially. See LICENSE.
TypeScript
98.6%
Self-hosted notification infrastructure for product notifications. One API call handles email, SMS, push, and webhooks, with preferences, quiet hours, retries, fallback, scheduling, workflows, and delivery logs built in.
110
stars
56
commits
TypeScript
primary language
Sep 8, 2026
updated
You shouldn't have to build a notification system.
Self-hosted notification infrastructure for product notifications. One API call handles email, SMS, push, and webhooks, with preferences, quiet hours, retries, fallback, scheduling, workflows, and delivery logs built in.
await sendEmail({
to: user.email,
subject: "Order Shipped",
...
});
Sending them reliably is the hard part. Users opt out. People are asleep. Push tokens die. Providers throw 503s. Channels fail and need fallback. Somewhere along the way you need timezone-aware quiet hours, future scheduling, deduplication, multi-channel templates, delivery logs, unsubscribe handling, multi-step workflows, and a dead-letter queue nobody wants to maintain.
notifkit is that machinery. Your app makes one typed call, and notifkit decides who gets the notification, which channel to use, when to send it, whether the user is allowed to receive it, and what to do when delivery fails.
import { notifkit } from "notifkit";
await notifkit.notify({
user: "usr_123",
template: "order-shipped",
channels: ["push", "email"],
fallback: true,
});
That call tries push, then email if push fails. Preferences, consent, quiet hours, retries, deduplication, throttling, template rendering, and delivery tracking all happen behind it.
notifkit is an orchestration engine and a typed SDK.
flowchart TD
App["Your Application / AI Agent<br/>Typed SDK · REST API · MCP Server"]
App -->|"HTTP POST /v1/notify"| API
API["Notifkit API Server<br/>Schema Validation · Auth · Multi-Tenancy<br/>Idempotency Gate · Priority Queue Ingestion"]
API --> PG
API --> REDIS
PG[("PostgreSQL — Storage<br/>Users · Preferences<br/>Templates · Workflows<br/>Delivery Logs · DLQ")]
REDIS[("Redis — Streams / ZSET<br/>Priority Queues<br/>Scheduled Sends<br/>Sliding Rate Limits")]
subgraph WORKERS["Background Workers Pipeline"]
direction LR
ENRICH["Enricher<br/>(Resolve)"] --> ENGINE["Engine<br/>(Quiet Hours)"] --> DELIVER["Delivery<br/>(Rate Limits / CB)"]
ENGINE --> SCHED["Scheduler<br/>(sendAt / QH)"]
SCHED --> DELIVER
end
REDIS -->|"consume"| ENRICH
ENRICH -.->|"read / write state"| PG
DELIVER -.->|"delivery logs"| PG
DELIVER -->|"Dispatch"| PROVIDERS
PROVIDERS["Provider Transports<br/>Email: Resend · Push: Firebase (FCM) · SMS: Twilio<br/>Chat: Slack, Telegram, Discord, WhatsApp<br/>Webhooks: Custom HTTP"]
classDef entry stroke:#6366f1,stroke-width:2px
classDef store stroke:#0ea5e9,stroke-width:2px
classDef work stroke:#22c55e,stroke-width:2px
class App,API,PROVIDERS entry
class PG,REDIS store
class ENRICH,ENGINE,DELIVER,SCHED work
NotifkitServer runs the HTTP REST API router (/v1/notify, /health, /metrics) and the background worker pipelines: enricher, decision engine, scheduler, and delivery. NotifkitClient is the lightweight client your application uses to trigger notifications, sync templates, and manage users over HTTP.
In a single process, the API and all workers run in the same Node.js process (services: ["all"]), which works for small and medium apps, side projects, and staging. Distributed, you run stateless API servers (services: ["api"]) behind a load balancer and scale worker pools (services: ["enricher", "engine", "delivery", "scheduler"]) horizontally across Redis Streams consumer groups.
npm install notifkit @notifkit/provider-resend
npm install -D tsx @testcontainers/postgresql @testcontainers/redis
The two @testcontainers/* packages are what notifkit uses to start throwaway PostgreSQL and Redis containers in development. They are imported lazily, only when the server is given neither a databaseUrl/redisUrl option nor a DATABASE_URL/REDIS_URL environment variable, so devDependencies is the right place for them.
[!WARNING] Those containers are for local development only. They are thrown away when the process exits, taking every user, template, delivery log, and queued notification with them. Before you deploy, point notifkit at a real PostgreSQL and Redis — set
DATABASE_URLandREDIS_URL(or passdatabaseUrlandredisUrl) and run withNODE_ENV=production, which refuses to start a container and fails loudly if either is missing.
server.ts starts the API and the worker pipelines. In development it auto-starts those containers, so Docker is the only prerequisite.
// server.ts
import { NotifkitServer } from "notifkit";
import { ResendTransport } from "@notifkit/provider-resend";
const server = new NotifkitServer({
services: ["all"], // API + enricher + engine + scheduler + delivery
port: 3000,
providers: [
new ResendTransport({
apiKey: process.env.RESEND_API_KEY!,
from: "notifications@yourdomain.com",
}),
],
});
await server.start();
console.log("notifkit listening on http://localhost:3000");
from is required on ResendTransport — it is the sender for any template that does not name its own, and it has to be an address on a domain you have verified in Resend. A template can override it with its own from, so one transport can serve both no-reply@ receipts and marketing@ campaigns.
ADMIN_API_KEY is the root credential. It is read from the environment, it is what mints project API keys in the next step, and without it the project-management routes answer 403. Any string works locally:
ADMIN_API_KEY=supersecretkey RESEND_API_KEY=re_xxx npx tsx server.ts
[!WARNING]
supersecretkeyis a local placeholder. In production this one value can mint keys for every project, so use a long random string kept in your secret store —openssl rand -hex 32is enough.
Every /v1/* route requires a project API key, and only the admin credential can mint one, so this is the single bootstrap step between a running server and your first notification:
ADMIN_API_KEY=supersecretkey npx notifkit-create-project "my-app"
Project "my-app" created. Save the API key now — it is not recoverable.
NOTIFKIT_PROJECT_ID=1ce67fa1-b4a9-4985-8046-ef6018912b2a
NOTIFKIT_API_KEY=nk_live_f57c57b76d795cef89e2dbf6b6f352a36…
The server stores only a SHA-256 hash of the key, so the nk_live_… value is printed once and never again — put it in your app's .env now. Point the script at another host with NOTIFKIT_URL, and mint further keys later with POST /v1/projects/:id/keys (role: "read_only" there gets you a key that can read but not send).
client.ts is your application code. It talks to the server over HTTP: register a template, register a user, and send.
// client.ts
import { NotifkitClient } from "notifkit";
const notifkit = new NotifkitClient({
baseUrl: "http://localhost:3000",
apiKey: process.env.NOTIFKIT_API_KEY!,
});
// 1. Register a template
await notifkit.syncTemplates({
templates: [
{
id: "order-shipped",
channel: "email",
content: { subject: "Order #{{orderId}} Shipped", text: "Your order is on the way!" },
},
],
});
// 2. Register a user
await notifkit.addUser({ id: "usr_123", email: "alex@acme.com" });
// 3. Dispatch
await notifkit.notify({
user: "usr_123",
template: "order-shipped",
channels: ["email"],
data: { orderId: "9481" },
});
With the server still running in the first terminal, run the client in a second one:
NOTIFKIT_API_KEY=nk_live_xxx npx tsx client.ts
The Node.js SDK is optional. notifkit exposes a standard HTTP REST API, so you can dispatch notifications and manage resources from any language (cURL, Python, Go, and so on). The same project API key goes in the Authorization header (an x-api-key header works too):
curl -X POST http://localhost:3000/v1/notify \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $NOTIFKIT_API_KEY" \
-d '{
"user": "usr_123",
"template": "order-shipped",
"channels": ["email"],
"data": { "orderId": "9481" }
}'
Locally, Docker is the only prerequisite: in development notifkit starts throwaway PostgreSQL and Redis containers for you. In production you need Node 22+, PostgreSQL, and Redis, and you run migrations by pointing drizzle-kit at node_modules/notifkit/drizzle.
notifkit runs in production at my own company, delivering 100K+ notifications a day across email, push, and OTPs. I built it because I needed it and didn't want to spend months rebuilding distributed notification plumbing or pay a SaaS per alert. It runs on your servers, with your provider accounts and your data.
Every component of the pipeline is tested against failure:
flowchart LR
S1["Redis Streams"] -->|"Kill Worker (SIGKILL)"| M1["Auto-Claim and Replay"] --> O1["Zero Lost Messages"]
S2["Connection Loss"] -->|"Drop DB / Redis"| M2["Auto-Reconnect / Retry"] --> O2["In-Flight State Intact"]
S3["10k+ Messages"] -->|"Burst"| M3["Concurrency and Limits"] --> O3["Flat Memory, No Leaks"]
classDef fault stroke:#ef4444,stroke-width:2px
classDef guard stroke:#6366f1,stroke-width:2px
classDef result stroke:#22c55e,stroke-width:2px
class S1,S2,S3 fault
class M1,M2,M3 guard
class O1,O2,O3 result
tests/chaos/crash.test.ts): background worker processes are killed with SIGKILL during high-throughput message streaming. Consumer group Pending Entries List (PEL) re-claims mean no messages are lost and another worker takes over.tests/chaos/recovery.test.ts): PostgreSQL and Redis connections are severed and restored under live traffic, verifying client reconnection, worker backpressure, and durable state resumption.tests/chaos/load.test.ts): bursts of 10,000+ notifications across parallel worker pools, checking queue drain speed, sliding-window rate limiters, and memory use over time.tests/race-conditions.test.ts, tests/idempotency.test.ts): concurrent duplicate dispatches, overlapping quiet-hour boundary evaluations, atomic user updates, and 24-hour idempotency key deduplication.| The problem you don't want to build | How notifkit solves it |
|---|---|
| "Should this user receive it?" | User preferences, topic opt-outs, and consent gates |
| "Is this a bad time to send?" | Timezone-aware quiet hours that defer non-urgent sends |
| "What if push fails?" | Ordered multi-channel fallback (push, then email, then sms) |
| "What if my worker crashes?" | Redis Streams consumer groups, retries, and durable idempotency |
| "What if an event fires twice?" | 24-hour deduplication via idempotency keys |
| "Can I send this later?" | Priority scheduling with sendAt and cancellation before dispatch |
| "Can I send this 3 days after signup?" | Stateful multi-step workflows with wait and waitForEvent |
| "How do I know what happened?" | Queryable delivery logs, Prometheus metrics, and campaign reporting |
| "What happens when a provider goes down?" | Circuit breakers, exponential backoff, and DLQ replay |
| "What about bounces and spam complaints?" | RFC 8058 one-click unsubscribe and automatic hard-bounce suppression |
| "What if I don't want another SaaS holding my data?" | Fully self-hosted on your PostgreSQL and Redis |
You decide what to say. notifkit gets it there.
notifkit is the durable notification layer that runs inside your own stack. It is not a marketing automation suite, and it does not replace Customer.io, OneSignal, or SendGrid. You bring your own provider accounts and pay them directly.
First-party providers cover Resend, Firebase Cloud Messaging, Slack, Twilio, Telegram, Discord, and WhatsApp. Anything else is a Transport class with a send() method.
Novu is the established open-source project in this space, and if you want a notification platform with a dashboard, a visual workflow editor, and a drop-in in-app inbox component, use Novu. It is more mature, has a much larger community, and solves a broader problem.
notifkit is a narrower, more embeddable take on the same layer:
What notifkit does not have: an in-app notification center or inbox component, a web dashboard for non-engineers, digest aggregation, or Novu's provider catalog. If you need those, Novu is the better fit.
https://github.com/user-attachments/assets/4dff98bb-37d3-44b4-bf46-9607c1cd89b5
An AI agent can operate notifkit directly. Connect the notifkit MCP server (@notifkit/mcp) to Claude Code, Cursor, Claude Desktop, Gemini, or any MCP-compatible agent:
npx -y @notifkit/mcp
You: Why didn't usr_9182 receive their password reset?
Agent: The notification was suppressed because usr_9182's email
address has a hard-bounce suppression from yesterday.
Your application and your AI agents use the same notification infrastructure. Through MCP an agent can:
send_notification, send_campaign)get_delivery_logs, get_notification)list_scheduled, cancel_notification)list_campaigns, get_campaign_stats)list_templates, preview_template, upsert_template)list_users, get_user_preferences, update_user_preferences)create_workflow, trigger_workflow, get_workflow_run)list_suppressions, get_dead_letters, replay_dead_letter)| Without an agent | With the notifkit MCP server |
|---|---|
| Query the database for contact info, open Twilio or Resend or write a throwaway script, format the payload, check the user's timezone by hand, send it, and hope it delivered. | You: "Send an urgent update to alex@acme.com that his package was lost in transit and support is rushing a replacement. Text him if push doesn't deliver." Agent: Looks up alex@acme.com, renders the template, dispatches push with SMS fallback, bypasses quiet hours because the send is urgent, tracks delivery status, and confirms it reached his phone. |
Already have notification code scattered across your application? Point your coding agent at:
https://notifkit.dev/llms-full.txt
It can read notifkit's API from there, find ad-hoc notification code in your repository, and refactor it into notifkit calls.
| Channels | email, sms, push, webhook, telegram, discord, whatsapp, slack |
| Targeting | A user, a list of users, a segment, or a topic |
| Priorities | low, normal, high, critical, on separate stream lanes |
| Scheduling | Future sends with sendAt, quiet-hours deferral, cancellation |
| Preferences | Per-user channel and topic opt-outs, quiet hours, contact-level overrides |
| Workflows | Multi-step sequences with wait, waitForEvent, and notify steps |
| Reliability | Redis Streams, 24h idempotency, retries, DLQ, provider circuit breakers |
| Templates | {{var}} interpolation with destination-aware escaping |
| AI | Optional LLM augmentation before render via the Vercel AI SDK |
| Multi-tenancy | Projects with isolated keys, data, and rate limits |
| Consent | RFC 8058 one-click unsubscribe; complaints and hard bounces suppress automatically |
| Reporting | Campaign labels with delivery and engagement totals |
| Agent operation | MCP server for sending, triage, campaigns, templates, workflows, and system operations |
| Observability | Prometheus /metrics, /health, /live, /ready, and queryable delivery logs |
Bring your own provider accounts. First-party packages:
@notifkit/provider-resend: transactional email via Resend@notifkit/provider-fcm: push notifications via Firebase Cloud Messaging@notifkit/provider-slack: Slack messages via Incoming Webhooks or the Web API@notifkit/provider-twilio: SMS via Twilio, with signature-verified delivery status callbacks@notifkit/provider-telegram: messages via a Telegram bot@notifkit/provider-discord: messages via a Discord webhook@notifkit/provider-whatsapp: messages via Meta's WhatsApp Cloud APIFor anything else, implement a Transport:
class MyTransport implements Transport {
async send(message) {
// Send through SES, Postmark, APNs,
// SendGrid, a custom webhook, or anything else.
}
}
The keys, the billing, and the deliverability stay yours.
Everything lives at notifkit.dev/docs.
| Quickstart | Install to first delivered notification |
| How it works | Core concepts and the notification pipeline |
| Channels & fallback | Multicast, ordered fallback, and custom transports |
| Preferences & quiet hours | Preference, consent, and timing rules |
| Templates & AI | Interpolation, escaping, and per-channel content |
| Segments & scheduling | Fan-out, priority lanes, sendAt, and idempotency |
| Workflows | Multi-step sequences, recurring sends, and digests |
| Examples | Runnable projects |
| Architecture | Streams, delivery guarantees, topologies, and data model |
| Deployment | Docker, Compose, and production topologies |
| Operations | Health, metrics, DLQ, key rotation, and shutdown |
| Reference | API, payloads, and SDK methods |
| MCP server | Operate notifkit from an AI agent |
Notification infrastructure looks simple until you're responsible for it. Queues, retries, provider adapters, preference systems, quiet-hour logic, workflows, suppression handling, and operational tooling take months to build well. notifkit is what I built instead, and it's what I run.
Issues and pull requests are welcome. Stars help other people find the project.
npm install
npm run build
npm test
The test suite starts its own PostgreSQL and Redis containers, so Docker is the only thing you need running.
Questions, bugs, or ideas: contact.devkitshq@gmail.com, or open an issue.
MIT. Do what you like with it, including commercially. See LICENSE.
TypeScript
98.6%