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:

.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):

public/notify-sw.js
importScripts("https://api.getnotify.dev/sw.js");

2. Load notify.js

Load the script once in your root layout with next/script:

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://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.

app/enable-notifications.tsx
"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.

app/actions.ts
"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);
}
server
// 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.