BackGetting started

Quickstart

Scaffold an app, send your first notification, and watch it arrive in the inbox and as an email in about five minutes.

Before You Begin

You need Node.js 18 or later and npm. Nothing else: the starter app keeps everything in memory and uses a fake email provider, so there is no database to create and no API key to get.

  1. 01

    Create the app

    npx create-notifykit-app my-app
    cd my-app
    cp .env.example .env.local

    Unsubscribe links are signed with a secret. Generate one and paste it into .env.local as NOTIFYKIT_SECRET, replacing the placeholder:

    node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

    The app refuses to start while the placeholder is still there. Then install and run it:

    npm install
    npm run dev
  2. 02

    Try it

    Open http://localhost:3000 and choose Sign in as demo user. Fill in the form to send yourself a notification. It shows up in the inbox below, and the bell’s unread count goes up. Then open Manage your notification preferences and turn a channel off.

    Each send also produces an email. The fake provider accepts it and keeps it in memory instead of sending it, so nothing arrives in a real mailbox.

  3. 03

    Read lib/notifykit.ts

    Everything starts in one file. It defines the notification the form sends and creates the notify instance the rest of the app imports:

    lib/notifykit.ts
    const inbox = channel.inbox()
    const email = channel.email()
    
    export const commentMentioned = notification({
    	id: "comment_mentioned",
    	payload: { actorName: "string", postTitle: "string", postUrl: "string" },
    	channels: [
    		inbox({
    			title: "{{actorName}} mentioned you",
    			body: "In {{postTitle}}",
    			actionUrl: "{{postUrl}}",
    		}),
    		email({
    			subject: "{{actorName}} mentioned you in {{postTitle}}",
    			body:
    				`{{actorName}} mentioned you in "{{postTitle}}". ` +
    				"Open {{postUrl}} to reply.",
    		}),
    	],
    })
    
    export const notify = createNotifyKit({
    	notifications: [commentMentioned] as const,
    	database: memoryAdapter(),
    	providers: { email: fakeEmailProvider() },
    	unsubscribe, // { secret, baseUrl } from .env.local
    })

    That is abridged: the real file also checks the secret and seeds the demo user.

    The payload declares the data each send must include. Channel templates fill in {{placeholders}} from it, and TypeScript checks every send() against it.

  4. 04

    Add a notification

    Define a second one below the first and register it:

    lib/notifykit.ts
    export const taskAssigned = notification({
    	id: "task_assigned",
    	payload: { assignerName: "string", taskTitle: "string", taskUrl: "string" },
    	channels: [
    		inbox({
    			title: "{{assignerName}} assigned you a task",
    			body: "{{taskTitle}}",
    			actionUrl: "{{taskUrl}}",
    		}),
    		email({
    			subject: "New task: {{taskTitle}}",
    			body:
    				"{{assignerName}} assigned you {{taskTitle}}. " +
    				"Open it: {{taskUrl}}",
    		}),
    	],
    })
    
    export const notify = createNotifyKit({
    	notifications: [commentMentioned, taskAssigned] as const,
    	// ...
    })

    Save, and the new notification appears on the preferences page. A template that uses a field the payload does not declare is caught when the app starts, with the fix in the error.

  5. 05

    Send it

    Call notify.send() from server code with a recipient, the notification id, and its payload. A misspelled id or a missing field is a type error:

    const result = await notify.send({
    	recipientId: "demo_user",
    	notificationId: "task_assigned",
    	payload: {
    		assignerName: "Rey",
    		taskTitle: "Fix the auth bug",
    		taskUrl: "/tasks/42",
    	},
    })
    
    result.inboxItems // the inbox row that was written
    result.deliveries // the email, with status "sent"
    result.skipped // channels that did not go out, and why

    Turn email off for this notification on the preferences page and send again: result.skipped now lists email with the reason "preferences_disabled", while the inbox item is still written.

Going to Production

Three things change before real users see it:

  1. The database. Replace memoryAdapter() with the Drizzle adapter for SQLite or Postgres. See Database adapters.
  2. The email provider. Replace fakeEmailProvider() with resendProvider() or an SMTP provider. See Email and webhook providers.
  3. Auth. lib/session.ts reads an unsigned demo cookie that anyone can forge. Make identify() return the user from your real session. See Next.js.
lib/notifykit.ts
import { drizzlePostgresAdapter } from "@notifykitjs/drizzle"
import { resendProvider } from "@notifykitjs/resend"

export const notify = createNotifyKit({
	notifications: [commentMentioned, taskAssigned] as const,
	database: drizzlePostgresAdapter(db),
	providers: {
		email: resendProvider({
			apiKey: process.env.RESEND_API_KEY!,
			from: process.env.RESEND_FROM!,
		}),
	},
	unsubscribe: {
		secret: process.env.NOTIFYKIT_SECRET!,
		baseUrl: "https://yourapp.com/api/notifykit",
	},
})

The definitions, the route handler, and the React code stay the same. Before relying on it for anything critical, read Production readiness: the default queue is not durable.

First-Run Problems

SymptomFix
Error about the placeholder secretGenerate a secret with the command in step 1 and replace NOTIFYKIT_SECRET in .env.local
Unknown recipientThe recipient has to exist first. The starter creates demo_user when you sign in; for other users call upsertRecipient()
No email arrivedExpected: the fake provider keeps emails in memory. Switch to a real provider to send them
A new send is missing from the inboxThe send ran in a different module instance than the handler; see the note in step 5
Invalid NotifyKit configurationA definition has a problem. The error lists each one with a fix

For anything else, notify.explain() runs the same checks as send() without writing or sending anything, and says what each channel would do. See Explain and dry run.

Where Next