How to Set Up Push Notifications in Next.js
A practical walkthrough for adding web push to a Next.js 14+ App Router project — service worker placement, env vars, opt-in UX, and sending from server actions.
DotSpheres Engineering
Engineering, ReachBell ·
Next.js is the most common stack we see new ReachBell customers ship on, and the App Router (Next 14+) shifts a few things — most importantly, where the service worker file lives and how you trigger pushes from server actions. Here is the full setup.
Assumed: Next.js 14 or 15, App Router, TypeScript, deployed on Vercel or any HTTPS host.
1. Install the SDK
npm install @reachbell/webThe package is small and tree-shakes — only the pieces you import end up in your client bundle.
2. Add the service worker file
Browsers require the service worker to live at the domain root. In Next.js App Router, anything in `/public` is served from `/`. Create `public/reachbell-sw.js`:
// public/reachbell-sw.js
importScripts("https://cdn.reachbell.com/sw/v1/reachbell-sw.js");That single line bootstraps the ReachBell service worker logic. You can extend it later (custom click handlers, offline UI) — for most projects you never need to touch it again.
3. Configure env vars
Set these in `.env.local` (and in Vercel project settings for prod):
NEXT_PUBLIC_REACHBELL_PROJECT=prj_xxxxxxxx
REACHBELL_API_KEY=sk_live_xxxxxxxxThe project ID is safe to expose (it appears in subscription requests). The API key stays server-side — never import it from a client component.
4. Initialise on the client
Create `app/providers.tsx` to register the SDK once at the root layout:
"use client";
import { useEffect } from "react";
import { ReachBell } from "@reachbell/web";
export function PushProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
ReachBell.init({
projectId: process.env.NEXT_PUBLIC_REACHBELL_PROJECT!,
serviceWorkerPath: "/reachbell-sw.js",
});
}, []);
return <>{children}</>;
}Then wrap it in `app/layout.tsx`:
import { PushProvider } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<PushProvider>{children}</PushProvider>
</body>
</html>
);
}5. Build a soft prompt component
Do not call the native browser prompt on first paint — your acceptance rate will tank. Wrap the request in your own UI:
"use client";
import { useState } from "react";
import { ReachBell } from "@reachbell/web";
export function EnablePush() {
const [status, setStatus] = useState<"idle" | "subscribed" | "denied">("idle");
const subscribe = async () => {
const result = await ReachBell.subscribe();
setStatus(result.granted ? "subscribed" : "denied");
};
if (status === "subscribed") return <p>You are all set ✓</p>;
return (
<button onClick={subscribe} className="btn-primary">
Get price-drop alerts
</button>
);
}6. Send a push from a server action
The point of pushing from Next.js is usually a server event — order confirmed, comment received, deploy finished. Server actions make this clean:
// app/actions/notify.ts
"use server";
import { ReachBellServer } from "@reachbell/server";
const rb = new ReachBellServer({
apiKey: process.env.REACHBELL_API_KEY!,
});
export async function notifyOrderShipped(userId: string, orderId: string) {
await rb.send({
audience: { externalId: userId },
title: "Order shipped 📦",
body: `Order #${orderId} is on its way.`,
url: `/orders/${orderId}`,
});
}Wire that into your checkout flow:
// app/checkout/confirm/page.tsx (server component)
import { notifyOrderShipped } from "@/app/actions/notify";
export default async function ConfirmPage({ searchParams }: { searchParams: { order: string } }) {
await notifyOrderShipped(currentUserId(), searchParams.order);
return <p>Order placed — push sent.</p>;
}7. Match push subscribers to your users
On the subscribe call, pass an `externalId` so the push subscription gets linked to your user account. That way you can target by user, not just by anonymous token:
await ReachBell.subscribe({
externalId: session.user.id,
attributes: {
plan: session.user.plan,
locale: session.user.locale,
},
});8. Local testing
Service workers run on localhost without HTTPS, but with one gotcha: Next.js dev mode serves the service worker fresh on every reload, which can confuse the browser. If you see "Failed to register service worker", hard-reload (Cmd+Shift+R) or unregister via Chrome DevTools → Application → Service Workers.
Production checklist
- Service worker file at `public/reachbell-sw.js`, content-type served as `application/javascript`.
- `NEXT_PUBLIC_REACHBELL_PROJECT` set in all environments. `REACHBELL_API_KEY` set only server-side.
- Soft prompt triggers on engagement, not page load. UTM defaults configured in the ReachBell dashboard.
- Transactional pushes wired from server actions or route handlers — never from client code where they could be spoofed.
That is the full integration. See the Next.js integration page for the official sample repo, or React-specific guidance if you are not on the App Router. Start with a free project — no card needed.
Put this playbook to work.
Push, email & automations — free for your first 1,000 subscribers.
Start free