Database
Your database, your schema
Postgres with any driver or Drizzle, MySQL, SQLite or MongoDB. One conformance suite, 87 cases, runs against every one of them.
Self-hosted notifications for TypeScript
In-app inbox, transactional email, web push and Telegram, running inside your app, stored in your database, with no per-notification bill. One config file, one send(), one hook.
await notify.send("commentReply", {
to: threadOwnerId,
payload: { authorName: "Dana", commentId: "c_123" },
// optional, makes a retry safe
dedupeKey: `commentReply:c_123:${threadOwnerId}`,
});Built for how you actually work
easy-ping shipped after every current model's training cutoff. Ask Cursor or Claude to wire it up cold and you'll get a confident, fluent, completely invented API.
So we ship the context. Grab a prompt below, paste it in, and your assistant writes against the real surface instead of guessing at it.
Why it exists
The parts everyone hand-rolls, gets subtly wrong, and then maintains forever.
Database
Postgres with any driver or Drizzle, MySQL, SQLite or MongoDB. One conformance suite, 87 cases, runs against every one of them.
Types
send() won't compile with the wrong shape. No payload of type any, rotting for two years.
Latency
It writes its rows and returns. A 300 ms provider round trip has no business on a comment POST. Unless you pick 'inline', which awaits it on purpose.
Delivery
Atomic claim, so two concurrent sweeps can't send the same email twice. Backoff with jitter, five attempts.
Push
VAPID and aes128gcm on Web Crypto for browsers, Expo's push service for iOS and Android, with receipts that prune uninstalled devices.
Telegram
POST /telegram/link returns a t.me link; the user taps it and every send lands in their chat. Blocked bots prune themselves.
Realtime
One event stream per browser, other tabs mirror it, polling only as the fallback. Ten open tabs cost one connection.
Plugins
A per-type, per-channel preference center and timezone-aware digests, both as opt-in plugins.
How it compares
The alternatives are good, and most of them are a platform. This one is a dependency, and that difference decides almost everything else.
| easy-pinga library | Novuself-hosted | Knock · Courierhosted SaaS | |
|---|---|---|---|
| What you deploy | Nothing. It runs inside the app you already have. | Four services. API, worker, dashboard and websocket. | Nothing. It runs in their cloud. |
| Infrastructure it adds | None | MongoDB, Redis and S3, on top of your own | None |
| Where notifications are stored | Your Postgres, MySQL, SQLite or MongoDB, in tables you can query | Novu's own MongoDB | Their database, reachable by API |
| Cost per notification | None | None, but you pay for the servers | Metered per send |
| License | MIT, all of it | MIT core, commercial enterprise modules | Proprietary |
| How you integrate | One config file, one send(), one hook | HTTP API plus an embeddable inbox | HTTP API plus an embeddable inbox |
| Visual workflow editor | No. A notification is code, reviewed like code | Yes | Yes |
| Channels out of the box | In-app, email, web push, mobile push, Telegram | Dozens of providers across every channel | Dozens of providers across every channel |
Checked 22 September 2026 against Novu's repository and self-hosting guide, and the Knock and Courier pricing pages. They are good tools solving a bigger problem. If you need a workflow editor that a non-developer can edit, use one of them.
Delivery
Modes are additive, not alternatives. The rows are committed before any of them runs, so a missing platform primitive or a crashed process costs latency, never a notification.
| Mode | Delivery happens | Latency | Needs cron |
|---|---|---|---|
inline | Before send() resolves | in-request | recommended |
deferred | After the response, via waitUntil | ~1 s | yes |
worker | In-process loop, woken by send() | instant | optional |
cron | When the sweep runs | up to the interval | yes |
Schema, config, one send(), and the bell. The quickstart is the whole thing, in order, with the gotchas called out.