Production readiness
What NotifyKit guarantees today, what it does not, and what to put around it before critical delivery.
Where It Stands
NotifyKit 0.0.x is a preview. The notification model, both database adapters, preferences, and the delivery pipeline are tested, and the API may still change before 0.1. What the preview does not yet give you is the infrastructure around delivery: a durable queue, migrations, bounce handling, and tools for recovering failed sends.
| Use | Setup | Today |
|---|---|---|
| Demos, local development, tests | Memory adapter and fake providers | Supported |
| A prototype with a persistent inbox | SQLite or Postgres with the default inline queue | Supported; sends include provider time |
| Everyday app notifications | Postgres, a durable queue, monitoring | Possible with your own integration work |
| Security, billing, or compliance messages | Outbox, durable worker, bounce handling, recovery | Wait for a stable release, or own those pieces |
| Marketing campaigns | Audiences, consent, campaign analytics | Use a platform built for it |
Queues
A send writes its records, then hands each email, SMS, or webhook to a queue as a DeliveryJob. The inbox is written directly and does not go through it.
| Queue | Behaviour | What can go wrong |
|---|---|---|
inlineQueue() (default) | send() waits for delivery, retries included | Slow providers slow your request; nothing is lost silently |
setTimeoutQueue() | send() returns first; delivery runs moments later in the same process | A crash, deploy, or frozen serverless function drops it |
| Your own durable queue | Jobs are stored and a worker runs them | As reliable as your queue and worker |
With the default retry policy, a failing provider is tried three times, waiting 250 ms and then 1 s, so an inline send can take over a second longer when a provider is down. Change it with retry on createNotifyKit().
A Durable Queue
A queue is two functions: enqueue(job, run) and drain(). A durable one stores the job and ignores run; a worker later passes the job to notify.processDeliveryJob(). Jobs are plain JSON. With BullMQ:
import { Queue as BullQueue } from "bullmq"
import type { Queue } from "@notifykitjs/core"
const deliveries = new BullQueue("notifykit", { connection })
const durableQueue: Queue = {
async enqueue(job) {
// deliveryId as the job id: a retried enqueue does not add a second job
await deliveries.add("deliver", job, { jobId: job.deliveryId })
},
async drain() {},
}
export const notify = createNotifyKit({ /* ... */ queue: durableQueue })import { Worker } from "bullmq"
import { notify } from "./lib/notifykit"
new Worker("notifykit", (job) => notify.processDeliveryJob(job.data), { connection })With a queue like this, send() returns while its deliveries are still pending; read the final status later with notify.deliveries.list() or the delivery timeline.
processDeliveryJob() checks the job against its stored delivery record and refuses one that was edited. It skips a job whose delivery already finished, so a queue that delivers a job twice does not send it twice. There is still a small window: a process can crash after the provider accepted a message but before the success was recorded, and the retry sends it again. Delivery is at least once. Use your provider’s idempotency support where it has one, and make sure a duplicate is harmless.
Checklist
Before relying on NotifyKit for messages that must arrive:
- Run Postgres, and create the tables with versioned migrations reviewed alongside your own, not the table-creation helpers at startup. See Database adapters.
- Record the intent to notify in the same transaction as the event that causes it, or accept that a crash between the two loses the notification.
- Use a durable queue with idempotent job ids and a worker you monitor.
- Take in your provider’s delivery, bounce, and complaint webhooks, and stop sending to addresses that bounce hard or complain.
- Alert on failed deliveries, and practise the recovery before you need it.
- Load test with your own database, provider, and queue.
- Back up the NotifyKit tables, and test tenant isolation through your real authentication.
- On boot and on a schedule, call
notify.recoverScheduledSends()to deliver quiet-hours sends whose time passed while no process was running.
What Is Tested
The repository’s CI tests payload validation, preference resolution, tenant isolation, idempotency, deduplication, retries, fallbacks, inbox changes, realtime events, and both the SQLite and Postgres adapters. That covers the library. It cannot stand in for testing your own deployment.
Before a Stable Release
In order, these come before any new channels or providers:
- Durable dispatch and a transactional outbox, so delivery intent survives deploys and crashes
- Versioned Drizzle migrations for every schema change
- Bounce and complaint handling that updates delivery status and stops repeat sends
- Recovery tools to list failures and safely retry, resend, or cancel them
- Pagination everywhere, and published, reproducible benchmarks
- Every documented package on npm, including
@notifykitjs/nodemailer
The Roadmap page has the rest, and the issue board tracks each item.