Next.js
Add native browser push to a Next.js App Router app. Four small pieces — a service worker file, the script, a subscribe button, and a server action to send.
Prerequisites
Get your keys with npx notify-dev init, then add them to .env.local:
# publishable key — safe in the browser NEXT_PUBLIC_NOTIFY_KEY=ntfy_pk_your_publishable_key # secret key — server-side only, never NEXT_PUBLIC_ NOTIFY_SECRET=ntfy_sk_your_secret_key
Two keys: the publishable key goes in the browser (script tag); the secret key stays on your server and is the only one that can send. Never put the secret key in a NEXT_PUBLIC_ variable.
1. Add the service worker
Create public/notify-sw.js (Next serves it at /notify-sw.js, same-origin with your pages):
importScripts("https://api.getnotify.dev/sw.js");2. Load notify.js
Load the script once in your root layout with next/script:
import Script from "next/script";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Script
src={"https://api.getnotify.dev/notify.js?token=" + process.env.NEXT_PUBLIC_NOTIFY_KEY}
strategy="afterInteractive"
/>
</body>
</html>
);
}3. Subscribe a user
A client component — call it after the user chooses to opt in.
"use client";
declare global {
interface Window {
notify?: { subscribe: (userId: string) => Promise<unknown> };
}
}
export function EnableNotifications({ userId }: { userId: string }) {
return (
<button onClick={() => window.notify?.subscribe(userId).catch(console.error)}>
Enable notifications
</button>
);
}4. Send from the server
A server action keeps the call server-side. Trigger it from anywhere on your backend.
"use server";
export async function notifyUser(userId: string, body: string) {
const res = await fetch("https://api.getnotify.dev/send", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
token: process.env.NOTIFY_SECRET,
userId,
title: "Acme",
body,
}),
});
if (!res.ok) throw new Error("notify: send failed " + res.status);
}// e.g. when a background job finishes await notifyUser(user.id, "Your export is ready!");
That’s the whole loop. If nothing appears: confirm notifications are allowed for your site and that /notify-sw.js loads at your domain root.