notifkit 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -160
- package/dist/index.d.mts +6 -6
- package/package.json +6 -1
- package/scripts/create-project.mjs +61 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**You shouldn't have to build a notification system.**
|
|
6
6
|
|
|
7
|
-
Self-hosted notification infrastructure for product notifications. One API call handles email, SMS, push, and webhooks
|
|
7
|
+
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.
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/notifkit) [](https://www.npmjs.com/package/notifkit) [](https://github.com/devkitshq/notifkit/actions/workflows/ci.yml) [](https://www.typescriptlang.org/) [](https://nodejs.org) [](./LICENSE)
|
|
10
10
|
|
|
@@ -12,9 +12,7 @@ Self-hosted notification infrastructure for product notifications. One API call
|
|
|
12
12
|
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
### The first notification is easy
|
|
15
|
+
### Sending one notification is easy
|
|
18
16
|
|
|
19
17
|
```ts
|
|
20
18
|
await sendEmail({
|
|
@@ -24,13 +22,9 @@ await sendEmail({
|
|
|
24
22
|
});
|
|
25
23
|
```
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Users opt out. People are asleep. Push tokens die. Providers throw 503s. Some channels fail and need fallback. 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.
|
|
25
|
+
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.
|
|
30
26
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Your app makes one typed call. notifkit handles the rest — **who gets it, which channel to use, when to send it, whether they're allowed to receive it, and what happens when delivery fails.**
|
|
27
|
+
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.
|
|
34
28
|
|
|
35
29
|
```ts
|
|
36
30
|
import { notifkit } from "notifkit";
|
|
@@ -43,15 +37,11 @@ await notifkit.notify({
|
|
|
43
37
|
});
|
|
44
38
|
```
|
|
45
39
|
|
|
46
|
-
|
|
47
|
-
>
|
|
48
|
-
> Preferences, consent, quiet hours, retries, deduplication, throttling, template rendering, and delivery tracking happen behind that single call.
|
|
49
|
-
|
|
50
|
-
---
|
|
40
|
+
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.
|
|
51
41
|
|
|
52
|
-
##
|
|
42
|
+
## How it runs
|
|
53
43
|
|
|
54
|
-
notifkit is
|
|
44
|
+
notifkit is an orchestration engine and a typed SDK.
|
|
55
45
|
|
|
56
46
|
```mermaid
|
|
57
47
|
flowchart TD
|
|
@@ -79,7 +69,7 @@ flowchart TD
|
|
|
79
69
|
DELIVER -.->|"delivery logs"| PG
|
|
80
70
|
DELIVER -->|"Dispatch"| PROVIDERS
|
|
81
71
|
|
|
82
|
-
PROVIDERS["Provider Transports<br/>Email: Resend
|
|
72
|
+
PROVIDERS["Provider Transports<br/>Email: Resend · Push: Firebase (FCM) · SMS: Twilio<br/>Chat: Slack, Telegram, Discord, WhatsApp<br/>Webhooks: Custom HTTP"]
|
|
83
73
|
|
|
84
74
|
classDef entry stroke:#6366f1,stroke-width:2px
|
|
85
75
|
classDef store stroke:#0ea5e9,stroke-width:2px
|
|
@@ -89,99 +79,90 @@ flowchart TD
|
|
|
89
79
|
class ENRICH,ENGINE,DELIVER,SCHED work
|
|
90
80
|
```
|
|
91
81
|
|
|
92
|
-
|
|
93
|
-
- **`NotifkitClient`**: The lightweight client your application uses to trigger notifications, sync templates, and manage users over HTTP.
|
|
82
|
+
`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.
|
|
94
83
|
|
|
95
84
|
### Topologies
|
|
96
85
|
|
|
97
|
-
|
|
98
|
-
- **Distributed Services**: 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.
|
|
99
|
-
|
|
100
|
-
---
|
|
101
|
-
|
|
102
|
-
## Agent-operable
|
|
86
|
+
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.
|
|
103
87
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
[▶️ Watch the AI demo](assets/ai_demo.mp4) - this is link to raw video file
|
|
107
|
-
|
|
108
|
-
**notifkit isn't just an API your application can call — your AI agent can operate it directly.**
|
|
88
|
+
## Quickstart
|
|
109
89
|
|
|
110
|
-
|
|
90
|
+
### 1. Install
|
|
111
91
|
|
|
112
92
|
```bash
|
|
113
|
-
|
|
93
|
+
npm install notifkit @notifkit/provider-resend
|
|
94
|
+
npm install -D tsx @testcontainers/postgresql @testcontainers/redis
|
|
114
95
|
```
|
|
115
96
|
|
|
116
|
-
|
|
97
|
+
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.
|
|
117
98
|
|
|
118
|
-
|
|
119
|
-
|
|
99
|
+
> [!WARNING]
|
|
100
|
+
> 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_URL` and `REDIS_URL` (or pass `databaseUrl` and `redisUrl`) and run with `NODE_ENV=production`, which refuses to start a container and fails loudly if either is missing.
|
|
120
101
|
|
|
121
|
-
|
|
122
|
-
address has a hard-bounce suppression from yesterday.
|
|
123
|
-
```
|
|
102
|
+
### 2. Run the engine
|
|
124
103
|
|
|
125
|
-
|
|
104
|
+
`server.ts` starts the API and the worker pipelines. In development it auto-starts those containers, so Docker is the only prerequisite.
|
|
126
105
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
- **Template management** — List, preview, and update templates with sample data (`list_templates`, `preview_template`, `upsert_template`)
|
|
132
|
-
- **Users & preferences** — Look up users, contacts, preferences, and segment membership (`list_users`, `get_user_preferences`, `update_user_preferences`)
|
|
133
|
-
- **Workflow operations** — Trigger workflows and inspect workflow runs (`create_workflow`, `trigger_workflow`, `get_workflow_run`)
|
|
134
|
-
- **Suppressions & health** — Manage bounce suppressions, check system queues, and replay dead-letter messages (`list_suppressions`, `get_dead_letters`, `replay_dead_letter`)
|
|
106
|
+
```ts
|
|
107
|
+
// server.ts
|
|
108
|
+
import { NotifkitServer } from "notifkit";
|
|
109
|
+
import { ResendTransport } from "@notifkit/provider-resend";
|
|
135
110
|
|
|
136
|
-
|
|
111
|
+
const server = new NotifkitServer({
|
|
112
|
+
services: ["all"], // API + enricher + engine + scheduler + delivery
|
|
113
|
+
port: 3000,
|
|
114
|
+
providers: [
|
|
115
|
+
new ResendTransport({
|
|
116
|
+
apiKey: process.env.RESEND_API_KEY!,
|
|
117
|
+
from: "notifications@yourdomain.com",
|
|
118
|
+
}),
|
|
119
|
+
],
|
|
120
|
+
});
|
|
137
121
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
122
|
+
await server.start();
|
|
123
|
+
console.log("notifkit listening on http://localhost:3000");
|
|
124
|
+
```
|
|
141
125
|
|
|
142
|
-
|
|
126
|
+
`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:
|
|
143
127
|
|
|
144
|
-
|
|
128
|
+
```bash
|
|
129
|
+
ADMIN_API_KEY=supersecretkey RESEND_API_KEY=re_xxx npx tsx server.ts
|
|
130
|
+
```
|
|
145
131
|
|
|
146
|
-
|
|
132
|
+
> [!WARNING]
|
|
133
|
+
> `supersecretkey` is 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 32` is enough.
|
|
147
134
|
|
|
148
|
-
|
|
135
|
+
### 3. Create a project and its API key
|
|
149
136
|
|
|
150
|
-
|
|
137
|
+
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:
|
|
151
138
|
|
|
152
|
-
```
|
|
153
|
-
|
|
139
|
+
```bash
|
|
140
|
+
ADMIN_API_KEY=supersecretkey npx notifkit-create-project "my-app"
|
|
154
141
|
```
|
|
155
142
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
---
|
|
143
|
+
```
|
|
144
|
+
Project "my-app" created. Save the API key now — it is not recoverable.
|
|
159
145
|
|
|
160
|
-
|
|
146
|
+
NOTIFKIT_PROJECT_ID=1ce67fa1-b4a9-4985-8046-ef6018912b2a
|
|
147
|
+
NOTIFKIT_API_KEY=nk_live_f57c57b76d795cef89e2dbf6b6f352a36…
|
|
148
|
+
```
|
|
161
149
|
|
|
162
|
-
|
|
150
|
+
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).
|
|
163
151
|
|
|
164
|
-
|
|
165
|
-
npm install notifkit @notifkit/provider-resend
|
|
166
|
-
```
|
|
152
|
+
### 4. Dispatch your first notification
|
|
167
153
|
|
|
168
|
-
|
|
154
|
+
`client.ts` is your application code. It talks to the server over HTTP: register a template, register a user, and send.
|
|
169
155
|
|
|
170
156
|
```ts
|
|
171
|
-
|
|
172
|
-
import {
|
|
157
|
+
// client.ts
|
|
158
|
+
import { NotifkitClient } from "notifkit";
|
|
173
159
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
port: 3000,
|
|
178
|
-
providers: [new ResendTransport({ apiKey: process.env.RESEND_API_KEY! })],
|
|
160
|
+
const notifkit = new NotifkitClient({
|
|
161
|
+
baseUrl: "http://localhost:3000",
|
|
162
|
+
apiKey: process.env.NOTIFKIT_API_KEY!,
|
|
179
163
|
});
|
|
180
|
-
await server.start();
|
|
181
|
-
|
|
182
|
-
// 2. Instantiate client and register a template
|
|
183
|
-
const notifkit = new NotifkitClient({ baseUrl: "http://localhost:3000" });
|
|
184
164
|
|
|
165
|
+
// 1. Register a template
|
|
185
166
|
await notifkit.syncTemplates({
|
|
186
167
|
templates: [
|
|
187
168
|
{
|
|
@@ -192,9 +173,10 @@ await notifkit.syncTemplates({
|
|
|
192
173
|
],
|
|
193
174
|
});
|
|
194
175
|
|
|
195
|
-
//
|
|
176
|
+
// 2. Register a user
|
|
196
177
|
await notifkit.addUser({ id: "usr_123", email: "alex@acme.com" });
|
|
197
178
|
|
|
179
|
+
// 3. Dispatch
|
|
198
180
|
await notifkit.notify({
|
|
199
181
|
user: "usr_123",
|
|
200
182
|
template: "order-shipped",
|
|
@@ -203,13 +185,20 @@ await notifkit.notify({
|
|
|
203
185
|
});
|
|
204
186
|
```
|
|
205
187
|
|
|
206
|
-
|
|
188
|
+
With the server still running in the first terminal, run the client in a second one:
|
|
207
189
|
|
|
208
|
-
|
|
190
|
+
```bash
|
|
191
|
+
NOTIFKIT_API_KEY=nk_live_xxx npx tsx client.ts
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 5. Or call the REST API directly
|
|
195
|
+
|
|
196
|
+
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):
|
|
209
197
|
|
|
210
198
|
```bash
|
|
211
199
|
curl -X POST http://localhost:3000/v1/notify \
|
|
212
200
|
-H "Content-Type: application/json" \
|
|
201
|
+
-H "Authorization: Bearer $NOTIFKIT_API_KEY" \
|
|
213
202
|
-d '{
|
|
214
203
|
"user": "usr_123",
|
|
215
204
|
"template": "order-shipped",
|
|
@@ -218,24 +207,17 @@ curl -X POST http://localhost:3000/v1/notify \
|
|
|
218
207
|
}'
|
|
219
208
|
```
|
|
220
209
|
|
|
221
|
-
### Development vs.
|
|
222
|
-
|
|
223
|
-
- **Local Development**: Docker is the only prerequisite. In development, notifkit starts throwaway PostgreSQL and Redis containers automatically.
|
|
224
|
-
- **Production**: Node 22+, PostgreSQL, Redis. Run migrations by pointing `drizzle-kit` at `node_modules/notifkit/drizzle`.
|
|
210
|
+
### Development vs. production
|
|
225
211
|
|
|
226
|
-
|
|
212
|
+
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`.
|
|
227
213
|
|
|
228
|
-
##
|
|
214
|
+
## Running in production
|
|
229
215
|
|
|
230
|
-
|
|
231
|
-
>
|
|
232
|
-
> It is the infrastructure we built because we needed it ourselves — rather than spending months reinventing distributed notification plumbing or paying SaaS tolls per alert.
|
|
216
|
+
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.
|
|
233
217
|
|
|
234
|
-
|
|
218
|
+
### Reliability and failure testing
|
|
235
219
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
Because notification delivery is mission-critical, every pipeline component is tested against extreme failure conditions:
|
|
220
|
+
Every component of the pipeline is tested against failure:
|
|
239
221
|
|
|
240
222
|
```mermaid
|
|
241
223
|
flowchart LR
|
|
@@ -251,45 +233,97 @@ flowchart LR
|
|
|
251
233
|
class O1,O2,O3 result
|
|
252
234
|
```
|
|
253
235
|
|
|
254
|
-
-
|
|
255
|
-
-
|
|
256
|
-
-
|
|
257
|
-
-
|
|
258
|
-
-
|
|
259
|
-
|
|
260
|
-
---
|
|
236
|
+
- Crash testing (`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.
|
|
237
|
+
- Infrastructure recovery (`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.
|
|
238
|
+
- Load testing (`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.
|
|
239
|
+
- Race conditions and concurrency (`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.
|
|
240
|
+
- Real containers, no mocks: unit, integration, and chaos suites all run against real PostgreSQL and Redis containers via [Testcontainers](https://testcontainers.com).
|
|
261
241
|
|
|
262
242
|
## What you get
|
|
263
243
|
|
|
264
|
-
| The problem you don't want to build
|
|
265
|
-
|
|
|
266
|
-
|
|
|
267
|
-
|
|
|
268
|
-
|
|
|
269
|
-
|
|
|
270
|
-
|
|
|
271
|
-
|
|
|
272
|
-
|
|
|
273
|
-
|
|
|
274
|
-
|
|
|
275
|
-
|
|
|
276
|
-
|
|
|
244
|
+
| The problem you don't want to build | How notifkit solves it |
|
|
245
|
+
| :--------------------------------------------------- | :------------------------------------------------------------------- |
|
|
246
|
+
| "Should this user receive it?" | User preferences, topic opt-outs, and consent gates |
|
|
247
|
+
| "Is this a bad time to send?" | Timezone-aware quiet hours that defer non-urgent sends |
|
|
248
|
+
| "What if push fails?" | Ordered multi-channel fallback (`push`, then `email`, then `sms`) |
|
|
249
|
+
| "What if my worker crashes?" | Redis Streams consumer groups, retries, and durable idempotency |
|
|
250
|
+
| "What if an event fires twice?" | 24-hour deduplication via idempotency keys |
|
|
251
|
+
| "Can I send this later?" | Priority scheduling with `sendAt` and cancellation before dispatch |
|
|
252
|
+
| "Can I send this 3 days after signup?" | Stateful multi-step workflows with `wait` and `waitForEvent` |
|
|
253
|
+
| "How do I know what happened?" | Queryable delivery logs, Prometheus metrics, and campaign reporting |
|
|
254
|
+
| "What happens when a provider goes down?" | Circuit breakers, exponential backoff, and DLQ replay |
|
|
255
|
+
| "What about bounces and spam complaints?" | RFC 8058 one-click unsubscribe and automatic hard-bounce suppression |
|
|
256
|
+
| "What if I don't want another SaaS holding my data?" | Fully self-hosted on your PostgreSQL and Redis |
|
|
257
|
+
|
|
258
|
+
You decide what to say. notifkit gets it there.
|
|
259
|
+
|
|
260
|
+
## Scope
|
|
261
|
+
|
|
262
|
+
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.
|
|
263
|
+
|
|
264
|
+
First-party providers cover Resend, Firebase Cloud Messaging, Slack, Twilio, Telegram, Discord, and WhatsApp. Anything else is a `Transport` class with a `send()` method.
|
|
265
|
+
|
|
266
|
+
### How this compares to Novu
|
|
267
|
+
|
|
268
|
+
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.
|
|
269
|
+
|
|
270
|
+
notifkit is a narrower, more embeddable take on the same layer:
|
|
271
|
+
|
|
272
|
+
- **A library first, a platform second.** notifkit is an npm package you can run inside your existing Node process. Novu self-hosts as a set of services (API, worker, WebSocket server, dashboard SPA) that you deploy and operate alongside your app.
|
|
273
|
+
- **Postgres, not MongoDB.** notifkit stores state in PostgreSQL with Drizzle migrations and queues in Redis Streams. If Postgres is already your database, there is no new datastore to run.
|
|
274
|
+
- **Workflows as code.** Multi-step sequences are typed TypeScript, versioned in your repo, rather than built in a visual editor.
|
|
275
|
+
- **MIT, all of it.** There is no open-core split. Novu is MIT at the core with enterprise features under a commercial license; notifkit has no feature held back from the self-hosted build.
|
|
276
|
+
- **MCP as a first-class interface.** Agents operate the same infrastructure your app uses, including triage and delivery-log inspection.
|
|
277
|
+
|
|
278
|
+
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.
|
|
279
|
+
|
|
280
|
+
## Agent-operable
|
|
281
|
+
|
|
282
|
+
https://github.com/user-attachments/assets/4dff98bb-37d3-44b4-bf46-9607c1cd89b5
|
|
283
|
+
|
|
284
|
+
An AI agent can operate notifkit directly. Connect the notifkit MCP server ([`@notifkit/mcp`](./packages/mcp)) to Claude Code, Cursor, Claude Desktop, Gemini, or any MCP-compatible agent:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
npx -y @notifkit/mcp
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Ask your agent
|
|
291
|
+
|
|
292
|
+
```text
|
|
293
|
+
You: Why didn't usr_9182 receive their password reset?
|
|
294
|
+
|
|
295
|
+
Agent: The notification was suppressed because usr_9182's email
|
|
296
|
+
address has a hard-bounce suppression from yesterday.
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Your application and your AI agents use the same notification infrastructure. Through MCP an agent can:
|
|
277
300
|
|
|
278
|
-
|
|
301
|
+
- Send one-off notifications or campaigns to users, lists, and segments (`send_notification`, `send_campaign`)
|
|
302
|
+
- Diagnose delivery issues by inspecting message histories, provider responses, and quiet hours (`get_delivery_logs`, `get_notification`)
|
|
303
|
+
- Schedule future sends and cancel pending notifications (`list_scheduled`, `cancel_notification`)
|
|
304
|
+
- Check delivery, open, click, bounce, and complaint metrics (`list_campaigns`, `get_campaign_stats`)
|
|
305
|
+
- List, preview, and update templates with sample data (`list_templates`, `preview_template`, `upsert_template`)
|
|
306
|
+
- Look up users, contacts, preferences, and segment membership (`list_users`, `get_user_preferences`, `update_user_preferences`)
|
|
307
|
+
- Trigger workflows and inspect workflow runs (`create_workflow`, `trigger_workflow`, `get_workflow_run`)
|
|
308
|
+
- Manage bounce suppressions, check system queues, and replay dead-letter messages (`list_suppressions`, `get_dead_letters`, `replay_dead_letter`)
|
|
279
309
|
|
|
280
|
-
|
|
310
|
+
### The same task, with and without an agent
|
|
281
311
|
|
|
282
|
-
|
|
312
|
+
| Without an agent | With the notifkit MCP server |
|
|
313
|
+
| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
314
|
+
| 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."_<br><br>**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. |
|
|
283
315
|
|
|
284
|
-
|
|
316
|
+
[MCP documentation](https://notifkit.dev/docs/mcp.html)
|
|
285
317
|
|
|
286
|
-
|
|
318
|
+
## AI-assisted migration
|
|
287
319
|
|
|
288
|
-
|
|
320
|
+
Already have notification code scattered across your application? Point your coding agent at:
|
|
289
321
|
|
|
290
|
-
|
|
322
|
+
```text
|
|
323
|
+
https://notifkit.dev/llms-full.txt
|
|
324
|
+
```
|
|
291
325
|
|
|
292
|
-
|
|
326
|
+
It can read notifkit's API from there, find ad-hoc notification code in your repository, and refactor it into notifkit calls.
|
|
293
327
|
|
|
294
328
|
## Feature matrix
|
|
295
329
|
|
|
@@ -297,7 +331,7 @@ First-party providers ship for Resend and Firebase Cloud Messaging. Anything els
|
|
|
297
331
|
| :------------------ | :------------------------------------------------------------------------------------- |
|
|
298
332
|
| **Channels** | `email`, `sms`, `push`, `webhook`, `telegram`, `discord`, `whatsapp`, `slack` |
|
|
299
333
|
| **Targeting** | A user, a list of users, a segment, or a topic |
|
|
300
|
-
| **Priorities** | `low`, `normal`, `high`, `critical
|
|
334
|
+
| **Priorities** | `low`, `normal`, `high`, `critical`, on separate stream lanes |
|
|
301
335
|
| **Scheduling** | Future sends with `sendAt`, quiet-hours deferral, cancellation |
|
|
302
336
|
| **Preferences** | Per-user channel and topic opt-outs, quiet hours, contact-level overrides |
|
|
303
337
|
| **Workflows** | Multi-step sequences with `wait`, `waitForEvent`, and `notify` steps |
|
|
@@ -310,23 +344,19 @@ First-party providers ship for Resend and Firebase Cloud Messaging. Anything els
|
|
|
310
344
|
| **Agent operation** | MCP server for sending, triage, campaigns, templates, workflows, and system operations |
|
|
311
345
|
| **Observability** | Prometheus `/metrics`, `/health`, `/live`, `/ready`, and queryable delivery logs |
|
|
312
346
|
|
|
313
|
-
---
|
|
314
|
-
|
|
315
347
|
## Providers
|
|
316
348
|
|
|
317
|
-
Bring your own provider accounts.
|
|
318
|
-
|
|
319
|
-
First-party packages:
|
|
349
|
+
Bring your own provider accounts. First-party packages:
|
|
320
350
|
|
|
321
|
-
- [`@notifkit/provider-resend`](./packages/provider-resend)
|
|
322
|
-
- [`@notifkit/provider-fcm`](./packages/provider-fcm)
|
|
323
|
-
- [`@notifkit/provider-slack`](./packages/provider-slack)
|
|
324
|
-
- [`@notifkit/provider-twilio`](./packages/provider-twilio)
|
|
325
|
-
- [`@notifkit/provider-telegram`](./packages/provider-telegram)
|
|
326
|
-
- [`@notifkit/provider-discord`](./packages/provider-discord)
|
|
327
|
-
- [`@notifkit/provider-whatsapp`](./packages/provider-whatsapp)
|
|
351
|
+
- [`@notifkit/provider-resend`](./packages/provider-resend): transactional email via Resend
|
|
352
|
+
- [`@notifkit/provider-fcm`](./packages/provider-fcm): push notifications via Firebase Cloud Messaging
|
|
353
|
+
- [`@notifkit/provider-slack`](./packages/provider-slack): Slack messages via Incoming Webhooks or the Web API
|
|
354
|
+
- [`@notifkit/provider-twilio`](./packages/provider-twilio): SMS via Twilio, with signature-verified delivery status callbacks
|
|
355
|
+
- [`@notifkit/provider-telegram`](./packages/provider-telegram): messages via a Telegram bot
|
|
356
|
+
- [`@notifkit/provider-discord`](./packages/provider-discord): messages via a Discord webhook
|
|
357
|
+
- [`@notifkit/provider-whatsapp`](./packages/provider-whatsapp): messages via Meta's WhatsApp Cloud API
|
|
328
358
|
|
|
329
|
-
For anything else, implement a
|
|
359
|
+
For anything else, implement a `Transport`:
|
|
330
360
|
|
|
331
361
|
```ts
|
|
332
362
|
class MyTransport implements Transport {
|
|
@@ -337,9 +367,7 @@ class MyTransport implements Transport {
|
|
|
337
367
|
}
|
|
338
368
|
```
|
|
339
369
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
---
|
|
370
|
+
The keys, the billing, and the deliverability stay yours.
|
|
343
371
|
|
|
344
372
|
## Documentation
|
|
345
373
|
|
|
@@ -361,27 +389,13 @@ Everything lives at [**notifkit.dev/docs**](https://notifkit.dev/docs/).
|
|
|
361
389
|
| [Reference](https://notifkit.dev/docs/reference.html) | API, payloads, and SDK methods |
|
|
362
390
|
| [MCP server](https://notifkit.dev/docs/mcp.html) | Operate notifkit from an AI agent |
|
|
363
391
|
|
|
364
|
-
---
|
|
365
|
-
|
|
366
392
|
## Why build this?
|
|
367
393
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
You can spend months building queues, retries, provider adapters, preference systems, quiet-hour logic, workflows, suppression handling, and operational tooling.
|
|
371
|
-
|
|
372
|
-
Or you can use the infrastructure we built for ourselves.
|
|
373
|
-
|
|
374
|
-
**notifkit exists so your team can spend its time building the product — not another notification platform.**
|
|
375
|
-
|
|
376
|
-
---
|
|
377
|
-
|
|
378
|
-
## Star the repo ⭐
|
|
379
|
-
|
|
380
|
-
If notifkit saves you a month or two you were about to spend building this yourself, **[give it a star on GitHub](https://github.com/devkitshq/notifkit)** — it's the cheapest way to help other people find it.
|
|
394
|
+
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.
|
|
381
395
|
|
|
382
396
|
## Contributing
|
|
383
397
|
|
|
384
|
-
Issues and pull requests are welcome.
|
|
398
|
+
Issues and pull requests are welcome. Stars help other people find the project.
|
|
385
399
|
|
|
386
400
|
```bash
|
|
387
401
|
npm install
|
|
@@ -393,10 +407,7 @@ The test suite starts its own PostgreSQL and Redis containers, so Docker is the
|
|
|
393
407
|
|
|
394
408
|
## Contact
|
|
395
409
|
|
|
396
|
-
Questions, bugs, or ideas
|
|
397
|
-
|
|
398
|
-
- **Email:** [contact.devkitshq@gmail.com](mailto:contact.devkitshq@gmail.com)
|
|
399
|
-
- **Book a 30-min call:** [calendly.com/contact-devkitshq/30min](https://calendly.com/contact-devkitshq/30min)
|
|
410
|
+
Questions, bugs, or ideas: [contact.devkitshq@gmail.com](mailto:contact.devkitshq@gmail.com), or open an issue.
|
|
400
411
|
|
|
401
412
|
## License
|
|
402
413
|
|
package/dist/index.d.mts
CHANGED
|
@@ -462,7 +462,6 @@ declare const NotificationEnrichedPayloadSchema: z.ZodObject<{
|
|
|
462
462
|
priority: "low" | "normal" | "high" | "critical";
|
|
463
463
|
projectId: string;
|
|
464
464
|
recipientId: string;
|
|
465
|
-
rawEventId: string;
|
|
466
465
|
templateVariables: Record<string, unknown>;
|
|
467
466
|
recipient: {
|
|
468
467
|
id: string;
|
|
@@ -485,6 +484,7 @@ declare const NotificationEnrichedPayloadSchema: z.ZodObject<{
|
|
|
485
484
|
pushToken?: string | undefined;
|
|
486
485
|
pushTokens?: string[] | undefined;
|
|
487
486
|
};
|
|
487
|
+
rawEventId: string;
|
|
488
488
|
aiPrompts?: Record<string, string> | undefined;
|
|
489
489
|
templateId?: string | undefined;
|
|
490
490
|
campaignId?: string | undefined;
|
|
@@ -495,7 +495,6 @@ declare const NotificationEnrichedPayloadSchema: z.ZodObject<{
|
|
|
495
495
|
priority: "low" | "normal" | "high" | "critical";
|
|
496
496
|
projectId: string;
|
|
497
497
|
recipientId: string;
|
|
498
|
-
rawEventId: string;
|
|
499
498
|
templateVariables: Record<string, unknown>;
|
|
500
499
|
recipient: {
|
|
501
500
|
id: string;
|
|
@@ -518,6 +517,7 @@ declare const NotificationEnrichedPayloadSchema: z.ZodObject<{
|
|
|
518
517
|
pushTokens?: string[] | undefined;
|
|
519
518
|
locale?: string | undefined;
|
|
520
519
|
};
|
|
520
|
+
rawEventId: string;
|
|
521
521
|
aiPrompts?: Record<string, string> | undefined;
|
|
522
522
|
templateId?: string | undefined;
|
|
523
523
|
campaignId?: string | undefined;
|
|
@@ -735,8 +735,8 @@ declare const NotificationDispatchedPayloadSchema: z.ZodObject<{
|
|
|
735
735
|
priority: "low" | "normal" | "high" | "critical";
|
|
736
736
|
projectId: string;
|
|
737
737
|
taskId: string;
|
|
738
|
-
recipientId: string;
|
|
739
738
|
enrichedEventId: string;
|
|
739
|
+
recipientId: string;
|
|
740
740
|
templateVariables: Record<string, unknown>;
|
|
741
741
|
renderedContent: {
|
|
742
742
|
content: Record<string, unknown>;
|
|
@@ -783,8 +783,8 @@ declare const NotificationDispatchedPayloadSchema: z.ZodObject<{
|
|
|
783
783
|
priority: "low" | "normal" | "high" | "critical";
|
|
784
784
|
projectId: string;
|
|
785
785
|
taskId: string;
|
|
786
|
-
recipientId: string;
|
|
787
786
|
enrichedEventId: string;
|
|
787
|
+
recipientId: string;
|
|
788
788
|
renderedContent: {
|
|
789
789
|
content: Record<string, unknown>;
|
|
790
790
|
attachments?: {
|
|
@@ -1041,8 +1041,8 @@ declare const NotificationAiPendingPayloadSchema: z.ZodObject<{
|
|
|
1041
1041
|
priority: "low" | "normal" | "high" | "critical";
|
|
1042
1042
|
aiPrompts: Record<string, string>;
|
|
1043
1043
|
projectId: string;
|
|
1044
|
-
recipientId: string;
|
|
1045
1044
|
enrichedEventId: string;
|
|
1045
|
+
recipientId: string;
|
|
1046
1046
|
templateVariables: Record<string, unknown>;
|
|
1047
1047
|
recipient: {
|
|
1048
1048
|
id: string;
|
|
@@ -1073,8 +1073,8 @@ declare const NotificationAiPendingPayloadSchema: z.ZodObject<{
|
|
|
1073
1073
|
priority: "low" | "normal" | "high" | "critical";
|
|
1074
1074
|
aiPrompts: Record<string, string>;
|
|
1075
1075
|
projectId: string;
|
|
1076
|
-
recipientId: string;
|
|
1077
1076
|
enrichedEventId: string;
|
|
1077
|
+
recipientId: string;
|
|
1078
1078
|
templateVariables: Record<string, unknown>;
|
|
1079
1079
|
recipient: {
|
|
1080
1080
|
id: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "notifkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Self-hosted notification infrastructure. One call delivers to email, SMS, push, and webhook — routed by preference, quiet hours, and consent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "devkitshq",
|
|
@@ -48,12 +48,17 @@
|
|
|
48
48
|
"dist",
|
|
49
49
|
"src",
|
|
50
50
|
"drizzle",
|
|
51
|
+
"scripts/create-project.mjs",
|
|
51
52
|
"README.md",
|
|
52
53
|
"LICENSE"
|
|
53
54
|
],
|
|
55
|
+
"bin": {
|
|
56
|
+
"notifkit-create-project": "./scripts/create-project.mjs"
|
|
57
|
+
},
|
|
54
58
|
"scripts": {
|
|
55
59
|
"build": "tsdown",
|
|
56
60
|
"dev": "tsdown --watch",
|
|
61
|
+
"create-project": "node scripts/create-project.mjs",
|
|
57
62
|
"test": "vitest run --coverage",
|
|
58
63
|
"test:watch": "vitest",
|
|
59
64
|
"lint": "eslint . --fix",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Create a project and print its API key.
|
|
4
|
+
*
|
|
5
|
+
* Every `/v1/*` route needs a project API key, and a project API key can only
|
|
6
|
+
* be minted with the admin credential, so this is the one bootstrap step
|
|
7
|
+
* between a running server and the first notification.
|
|
8
|
+
*
|
|
9
|
+
* ADMIN_API_KEY=supersecretkey npm run create-project
|
|
10
|
+
* ADMIN_API_KEY=supersecretkey npx notifkit-create-project "my-app"
|
|
11
|
+
*
|
|
12
|
+
* ADMIN_API_KEY is the same variable the server reads, so pass the value the
|
|
13
|
+
* server was started with.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const adminKey = process.env.ADMIN_API_KEY;
|
|
17
|
+
const baseUrl = (process.env.NOTIFKIT_URL || "http://localhost:3000").replace(/\/$/, "");
|
|
18
|
+
const name = process.argv[2] || process.env.PROJECT_NAME || "my-app";
|
|
19
|
+
|
|
20
|
+
function fail(message) {
|
|
21
|
+
console.error(`create-project: ${message}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!adminKey) {
|
|
26
|
+
fail(
|
|
27
|
+
"no admin credential. Set ADMIN_API_KEY to the same value the server was started with:\n" +
|
|
28
|
+
" ADMIN_API_KEY=supersecretkey npm run create-project",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let res;
|
|
33
|
+
try {
|
|
34
|
+
res = await fetch(`${baseUrl}/v1/projects`, {
|
|
35
|
+
method: "POST",
|
|
36
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${adminKey}` },
|
|
37
|
+
body: JSON.stringify({ name }),
|
|
38
|
+
});
|
|
39
|
+
} catch (err) {
|
|
40
|
+
fail(`could not reach the server at ${baseUrl} — is it running? (${err.message})`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (res.status === 401) {
|
|
44
|
+
fail("admin credential rejected. It must match the server's ADMIN_API_KEY exactly.");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (res.status === 403) {
|
|
48
|
+
fail("project management is disabled. Start the server with ADMIN_API_KEY set.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
fail(`server returned ${res.status}: ${await res.text()}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { id, apiKey } = await res.json();
|
|
56
|
+
|
|
57
|
+
// The server keeps only a hash of the key, so this is the only time the value
|
|
58
|
+
// itself exists anywhere. Print it in .env form to make that the obvious move.
|
|
59
|
+
console.log(`\nProject "${name}" created. Save the API key now — it is not recoverable.\n`);
|
|
60
|
+
console.log(`NOTIFKIT_PROJECT_ID=${id}`);
|
|
61
|
+
console.log(`NOTIFKIT_API_KEY=${apiKey}\n`);
|