Skip to content
Developers · 9 min read

Web push notifications in React and Next.js

React apps need the same three things as any site — a service worker, a subscription and a sender. Next.js makes the server half easy with route handlers or server actions.

Updated By the Pusharoo team
In short
  • Put sw.js in public/ so it’s served from your root.
  • Subscribe in a client component, triggered by a button click.
  • Send from a Next.js route handler or server action using the web-push package.
  • Prefix only the public VAPID key with NEXT_PUBLIC_; keep the private key server-only.

1. Service worker in public/

Anything in Next.js’s public/ folder (or Vite/CRA’s public/) is served from the site root, so public/sw.js becomes /sw.js. Use the handlers from service worker push events. If your framework also generates a worker (for example a PWA plugin), merge push into it with importScripts rather than registering two.

2. A subscribe hook

"use client"; import { useState } from "react"; const KEY = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!; const toBytes = (b64: string) => { const pad = "=".repeat((4 - (b64.length % 4)) % 4); const raw = atob((b64 + pad).replace(/-/g, "+").replace(/_/g, "/")); return Uint8Array.from([...raw].map(c => c.charCodeAt(0))); }; export function NotifyButton() { const [state, setState] = useState<"idle" | "on" | "denied">("idle"); async function enable() { const reg = await navigator.serviceWorker.register("/sw.js", { updateViaCache: "none" }); if ((await Notification.requestPermission()) !== "granted") return setState("denied"); const sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: toBytes(KEY) }); await fetch("/api/push", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(sub) }); setState("on"); } return <button onClick={enable} disabled={state === "on"}>{state === "on" ? "Alerts on" : "🔔 Get alerts"}</button>; }

3. A route handler that saves and sends

// app/api/push/route.ts import webpush from "web-push"; webpush.setVapidDetails( "mailto:[email protected]", process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY!, process.env.VAPID_PRIVATE_KEY! // server-only, never NEXT_PUBLIC_ ); export async function POST(req: Request) { const sub = await req.json(); await db.subscriptions.upsert(sub); // your database return new Response(null, { status: 201 }); } export async function sendToAll(msg: { title: string; body: string; url: string }) { for (const sub of await db.subscriptions.all()) { try { await webpush.sendNotification(sub, JSON.stringify(msg), { TTL: 3600 }); } catch (e: any) { if ([404, 410].includes(e.statusCode)) await db.subscriptions.remove(sub.endpoint); } } }

Route handlers using web-push must run on the Node.js runtime, not the Edge runtime, because the library relies on Node crypto. More in Node.js web push.

4. iPhone notes

  • Add a manifest with "display": "standalone" (Next.js supports app/manifest.ts).
  • On iPhone, the button only works inside the Home Screen web app; detect standalone mode and show install steps otherwise (PWA guide).
  • Check "PushManager" in window before rendering the button.

Or use a script tag

// app/layout.tsx import Script from "next/script"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"><body> {children} <Script src="https://pusharoo.com/sdk/pusharoo.js" data-site="pk_your_site_key" strategy="afterInteractive" /> </body></html> ); }

With Pusharoo’s sw.js in public/, that’s the whole integration: prompt, iPhone hint, subscriptions, campaigns and automations, free. Your own button can call window.Pusharoo.subscribe() (API).

Questions

Do I need next-pwa to send push notifications?

No. A plain service worker in public/ plus the Push API is enough. PWA plugins are optional, for offline caching and install prompts.

Can I send web push from the Next.js Edge runtime?

The web-push package needs Node.js APIs, so use the Node.js runtime for sending.

How do I test push in development?

Browsers treat http://localhost as secure, so service workers and push work locally. Use DevTools’ Push button to test the worker.

More in Developers: VAPID keys for web push, explained · The Web Push protocol, explained · Web push notifications in PHP · Laravel browser push notifications (no Firebase) · Declarative Web Push, explained · Firebase web push vs. standard web push · All guides

Free for life. No limits.

Unlimited subscribers, campaigns and automations. No badges. About five minutes to install.