Next.js walkthrough
From nothing to a notification you can see, in five commands.
See it running and inspect apps/demos/next.
An existing Next.js app on Postgres, from nothing to a notification you can see. No VAPID keys to mint by hand, no service worker to write, no device to own, and no certificate to configure.
You need a Next.js project with a DATABASE_URL pointing at a Postgres you can
write to. Everything else is below.
1. Scaffold
npx @better-push/cli initIt detects Next.js, your database client, your package manager, whether you use
src/, and whether @/* maps to it - then shows the plan and waits.
Detected Next.js (App Router) · Drizzle ORM · package manager pnpm · source root src
Planned changes
create src/push.ts
create src/app/api/push/[...all]/route.ts
create src/db/better-push-schema.ts
create public/sw.js
env .env.local (VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, VAPID_SUBJECT, SESSION_SECRET, …)Say yes.
2. Install
pnpm add @better-push/core pg3. Create the tables
npx @better-push/cli migrate database localhost:5432/app
+ bp_audit + bp_delivery + bp_device + bp_digest_window
+ bp_job + bp_metric_rollup + bp_migration + bp_notification
+ bp_preference + bp_rollup_state + bp_worker
11 table(s) created, 24 index(es) created.Want drizzle-kit to own these instead? init already wrote
src/db/better-push-schema.ts. Re-export it from your schema and run
drizzle-kit generate && drizzle-kit migrate - skip this step entirely.
4. Wire your session
This is the one thing no tool can do for you. Open src/push.ts and replace the
TODO:
session: async (request) => {
const session = await auth(request); // your auth, whatever it is
return session ? { userId: session.user.id } : null;
},Return { userId } for an authenticated request, null otherwise. The router
answers 401 for null, and every endpoint is scoped to that user.
Using better-auth? One line:
import { betterAuthSession } from "@better-push/core/auth/better-auth";
session: betterAuthSession(auth),5. Check it
npx @better-push/cli doctor ✓ Framework: Next.js (App Router)
✓ The mount file exists
✓ public/sw.js exists
✓ All 11 better-push tables exist
✓ VAPID keys are well-formed
! No queue is configured
! Nothing is ever deleted
9 ok 3 warning(s) 0 errorsThe warnings are folds you have not opened yet, not faults. Zero errors means it is wired up.
6. See a notification
Add the emulator provider - two lines, already commented in the file init
wrote:
import { emulator } from "@better-push/core/providers/emulator";
providers: [
webPush({ vapid: { /* ... */ } }),
...(process.env.NODE_ENV === "production" ? [] : [emulator()]),
],Start the inbox in a second terminal:
npx @better-push/cli devRegister a virtual device from any client component:
"use client";
import { useEmulatorDevice } from "@better-push/core/react";
export function DevTools() {
const emulator = useEmulatorDevice();
if (process.env.NODE_ENV === "production") return null;
return <button onClick={() => void emulator.register()}>Register device</button>;
}Click it, then send from a server action or a route:
await push.notify({ userId: "your-user-id", title: "Hello", body: "It works." });It appears at http://127.0.0.1:4984 immediately. No certificates, no
permission prompt, no device.
7. Real push, and real UI
Now that the pipeline works, turn on the parts that need the browser.
npx @better-push/cli add notification-bell notification-preferences push-toggle
npx shadcn@latest add badge button popover scroll-area skeleton switchimport { NotificationBell } from "@/components/better-push/notification-bell";
<header><NotificationBell /></header>import { PushToggle } from "@/components/better-push/push-toggle";
import { NotificationPreferences } from "@/components/better-push/notification-preferences";
export default function Page() {
return (
<>
<PushToggle applicationServerKey={process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!} />
<NotificationPreferences />
</>
);
}Add NEXT_PUBLIC_VAPID_PUBLIC_KEY to .env.local with the same value init
wrote as VAPID_PUBLIC_KEY - the browser needs it and NEXT_PUBLIC_ is how Next
exposes it.
Click Enable push notifications, allow the prompt, and send again. This time it arrives as a real OS notification as well as in the feed.