Push notifications

Push notifications

These steps cover standards-based Web Push, which is built in and dependency-free. For native iOS and Android delivery, use a provider instead.

iOS and Android

Web Push cannot register with APNs or FCM on its own. For native iOS and Android delivery, use a provider adapter and follow its guide:

1.Prepare Web Push

The hook needs a secure origin, an active service worker, and a VAPID public key. Localhost is treated as secure during development.

Register your service worker once near the root of the web application:

useEffect(() => {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js');
  }
}, []);

Place the worker at public/sw.js. It must listen for incoming push events and display a notification for production delivery.

2.Create the hook

import { usePushNotifications } from '@zoharyandrianome/crosshooks';

function NotificationsButton() {
  const notifications = usePushNotifications({
    applicationServerKey: process.env.NEXT_PUBLIC_VAPID_KEY,
  });

  // Render your UI here.
}

The VAPID public key is safe to expose. Its matching private key must remain on the server.

3.Handle unsupported platforms

Read isSupported before showing the subscription controls. It remains false during server rendering and on platforms that cannot use Web Push.

if (!notifications.isSupported) {
  return <p>Push notifications are not available on this device.</p>;
}

4.Subscribe and store the result

Call subscribe() from a direct user action. The browser or operating system may show a permission prompt.

async function enableNotifications() {
  const subscription = await notifications.subscribe();
  if (!subscription) return;

  await fetch('/api/push/subscriptions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(subscription),
  });
}

The Web Push subscription is a serializable object:

{
  endpoint: string;
  expirationTime: number | null;
  keys: { p256dh: string; auth: string };
}

Store the subscription with the authenticated user. A user may have several subscriptions across multiple browsers and devices.

5.Render the controls

return (
  <button
    disabled={!notifications.isSupported}
    onClick={async () => {
      if (notifications.isSubscribed) {
        await notifications.unsubscribe();
      } else {
        await enableNotifications();
      }
    }}
  >
    {notifications.isSubscribed
      ? 'Disable notifications'
      : 'Enable notifications'}
  </button>
);

If unsubscribing succeeds, also remove or deactivate that subscription in your backend.

6.Send notifications from a trusted backend

This client hook registers and unregisters devices. It does not securely send privileged push messages. Your backend must send through Web Push using the stored subscription and your VAPID key pair.

Never expose VAPID private keys in a browser bundle, public repository, or NEXT_PUBLIC_* variable.

7.Test the complete lifecycle

  1. Open the application through HTTPS or localhost.
  2. Enable notifications from a user-initiated button.
  3. Confirm that permission becomes granted.
  4. Confirm that a subscription reaches your backend.
  5. Send a test message from your backend.
  6. Test foreground, background, and closed application states.
  7. Unsubscribe and confirm the backend record is removed.