BackProduction

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.

UseSetupToday
Demos, local development, testsMemory adapter and fake providersSupported
A prototype with a persistent inboxSQLite or Postgres with the default inline queueSupported; sends include provider time
Everyday app notificationsPostgres, a durable queue, monitoringPossible with your own integration work
Security, billing, or compliance messagesOutbox, durable worker, bounce handling, recoveryWait for a stable release, or own those pieces
Marketing campaignsAudiences, consent, campaign analyticsUse 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.

QueueBehaviourWhat can go wrong
inlineQueue() (default)send() waits for delivery, retries includedSlow providers slow your request; nothing is lost silently
setTimeoutQueue()send() returns first; delivery runs moments later in the same processA crash, deploy, or frozen serverless function drops it
Your own durable queueJobs are stored and a worker runs themAs 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:

lib/notifykit.ts
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 })
worker.ts
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:

  1. Durable dispatch and a transactional outbox, so delivery intent survives deploys and crashes
  2. Versioned Drizzle migrations for every schema change
  3. Bounce and complaint handling that updates delivery status and stops repeat sends
  4. Recovery tools to list failures and safely retry, resend, or cancel them
  5. Pagination everywhere, and published, reproducible benchmarks
  6. Every documented package on npm, including @notifykitjs/nodemailer

The Roadmap page has the rest, and the issue board tracks each item.