Packages
Four packages ship alongside the core. Three are transports — the thing that actually hands a message to a provider — and one is an MCP server. A channel with no transport registered delivers nothing, so this is the list of what you get for free and what you still have to write.
| Package | Channel | Wraps | Use it for |
|---|---|---|---|
@notifkit/provider-resend | email | Resend + svix | Production email, with engagement webhooks. |
@notifkit/provider-fcm | push | Firebase Admin | Production push to iOS and Android. |
@notifkit/provider-console | configurable | nothing | Development and tests. Prints and reports success. |
@notifkit/mcp | — | MCP SDK | Driving NotifKit from an AI agent. Not a transport. |
SMS has no package. The channel works
everywhere else in the system — contacts, templates, targeting, quiet hours, fallback,
suppression — but nothing ships that talks to Twilio or Vonage, so a send fails with
no_transport until you register your own.
Nothing here is required to run NotifKit — the core has no provider dependency, which is
why firebase-admin and resend are not in its dependency tree. Each
package declares notifkit as a peer dependency and is registered by you at
boot, so an install that only sends email never pulls in the Firebase SDK.
By channel
Writing your own
Everything above implements the same Transport interface, and nothing about the
first-party packages is privileged — a transport you write registers the same way and is
picked up by the same registry. Register more than one for a channel and the highest priority
is tried first, with the rest as failover.
import { registerTransport } from "notifkit";
registerTransport(new ResendTransport({ /* … */ }), 10); // tried first
registerTransport(new PostmarkTransport({ /* … */ }), 5); // only if Resend fails
Two transports on one channel give you provider failover. They do not let you choose between them per send — the registry keys on channel alone, so the second is reached only when the first fails. Anything that varies per message, a sender address included, belongs in the template and is read by a single transport.
The full interface, including the webhook hooks and what
DeliveryResult.invalidToken does, is in
the reference, and
Channels & fallback walks through a worked example.