diff --git a/src/data/nav/pubsub.ts b/src/data/nav/pubsub.ts index 08bb68865a..dab7d73597 100644 --- a/src/data/nav/pubsub.ts +++ b/src/data/nav/pubsub.ts @@ -284,6 +284,10 @@ export default { name: 'Flutter', link: '/docs/push/getting-started/flutter', }, + { + name: 'Next.js', + link: '/docs/push/getting-started/nextjs', + }, ], }, { diff --git a/src/images/content/screenshots/getting-started/nextjs-push-getting-started-guide.png b/src/images/content/screenshots/getting-started/nextjs-push-getting-started-guide.png new file mode 100644 index 0000000000..3e3e60fd00 Binary files /dev/null and b/src/images/content/screenshots/getting-started/nextjs-push-getting-started-guide.png differ diff --git a/src/pages/docs/push/getting-started/nextjs.mdx b/src/pages/docs/push/getting-started/nextjs.mdx new file mode 100644 index 0000000000..7acff211db --- /dev/null +++ b/src/pages/docs/push/getting-started/nextjs.mdx @@ -0,0 +1,468 @@ +--- +title: "Getting started: Push Notifications in Next.js" +meta_description: "Get started with Ably Push Notifications in Next.js. Learn how to register a service worker, activate push on your client, handle incoming notifications, and send push messages from a Next.js app." +meta_keywords: "Push Notifications Next.js, Ably Push, Web Push, Service Worker, Next.js push notifications, Ably Push Notifications guide, realtime push Next.js, push notification example, Ably tutorial Next.js, device registration, push messaging" +--- + +This guide will get you started with Ably Push Notifications in a Next.js application using the App Router. + +You'll learn how to set up an Ably Realtime client with push notification support, register a service worker, activate push notifications, subscribe to channel-based push, send push notifications, and handle incoming notifications in both the service worker and the React component. + +## Prerequisites + +1. [Sign up](https://ably.com/signup) for an Ably account. +2. Create a [new app](https://ably.com/accounts/any/apps/new), and create your first API key in the **API Keys** tab of the dashboard. + * Your API key needs the `publish`, `subscribe` capabilities. + * Also add the `push-admin` capability if you're using the same API key to send a push notification. In production this would more likely be a server using a different API key. +3. Add a rule to a channel so you can test sending push notification via a channel. Select [**Rules**](https://ably.com/accounts/any/apps/any/app_namespaces) in the Ably dashboard, add a new rule and enable the **Push notifications** option. +4. Install [Node.js](https://nodejs.org/) 18 or higher. +5. A modern browser that supports the [Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) (Chrome, Firefox, or Edge recommended). + +### (Optional) Install Ably CLI + +Use the [Ably CLI](/docs/platform/tools/cli) as an additional client to quickly test Pub/Sub features and push notifications. + +1. Install the Ably CLI: + + +```shell +npm install -g @ably/cli +``` + + +2. Run the following to log in to your Ably account and set the default app and API key: + + +```shell +ably login +``` + + +### Create a Next.js project + +Create a new Next.js project using the official create command: + + +```shell +npx create-next-app@latest ably-push-tutorial --typescript --app --no-tailwind --eslint --src-dir +cd ably-push-tutorial +``` + + +Then install the Ably SDK and React hooks package: + + +```shell +npm install ably +``` + + +## Step 1: Set up Ably + +This step initializes the Ably Realtime client with push notification support and wraps the app in `AblyProvider` so that all child components can access the client via hooks. + +Because the Ably SDK runs in the browser, the component must not be server-side rendered. This guide uses Next.js [`dynamic`](https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading) with `ssr: false` to keep things simple, but this is not the recommended approach for production apps. For a more complete Next.js integration pattern, see the [ably-nextjs-fundamentals-kit](https://github.com/ably/ably-nextjs-fundamentals-kit). + +Create `src/app/push/page.tsx` as the entry point: + + +```react +'use client'; + +import dynamic from 'next/dynamic'; + +const PushApp = dynamic(() => import('./PushApp'), { ssr: false }); + +export default function PushPage() { + return ; +} +``` + + +Then create `src/app/push/PushApp.tsx`. Because this module is only ever loaded client-side (due to `ssr: false`), it is safe to instantiate the Ably client at module scope: + + +```react +'use client'; + +import * as Ably from 'ably'; +import AblyPushPlugin from 'ably/push'; +import { AblyProvider, ChannelProvider } from 'ably/react'; +import { useState } from 'react'; +import { PushActivationBanner } from './PushActivationBanner'; +import { ChannelSubscription } from './ChannelSubscription'; +import { NotificationLog } from './NotificationLog'; + +const CHANNEL_NAME = 'my-first-push-channel'; + +const client = new Ably.Realtime({ + key: '{{API_KEY}}', // Do not use an API key in production — use token authentication instead + clientId: 'push-tutorial-client', + plugins: { Push: AblyPushPlugin }, + pushServiceWorkerUrl: '/service-worker.js', +}); + +export default function PushApp() { + const [output, setOutput] = useState([]); + const [deviceId, setDeviceId] = useState(null); + + function log(message: string) { + setOutput((prev) => [...prev, message]); + } + + return ( + +
+

Ably Push Notifications — Next.js

+
+
+

Push Notifications

+ + + + +
+
+ {deviceId &&
Device ID: {deviceId}
} + setOutput([])} /> +
+
+
+
+ ); +} +``` +
+ +Key configuration options: + +- **`key`**: Your Ably API key. +- **`clientId`**: A unique identifier for this client. +- **`plugins`**: The `AblyPushPlugin` enables push notification support. +- **`pushServiceWorkerUrl`**: Path to the service worker file. In Next.js, files in `public/` are served from the root, so `/service-worker.js` maps to `public/service-worker.js`. + +`PushActivationBanner` sits directly under `AblyProvider` and handles device-level push activation. `ChannelSubscription` sits inside a `ChannelProvider`, which scopes it to `my-first-push-channel`. + +Create `src/app/push/NotificationLog.tsx` as a presentational component with no Ably dependency: + + +```react +export function NotificationLog({ output, onClear }: { output: string[]; onClear: () => void }) { + const buttonStyle = { padding: '12px 20px', margin: '5px 0', width: '100%', display: 'block', background: '#6c757d', color: '#fff', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px' }; + + return ( + <> +
+ {output.map((entry, i) => ( +

{entry}

+ ))} +
+ + + ); +} +``` +
+ +## Step 2: Set up push notifications
+ +Create `src/app/push/PushActivationBanner.tsx`. This component uses the `usePushActivation` hook to activate and deactivate the device. + + +```react +'use client'; + +import { useEffect } from 'react'; +import { usePushActivation } from 'ably/react'; + +export function PushActivationBanner({ onLog, onDeviceChange }: { onLog: (msg: string) => void; onDeviceChange: (id: string | null) => void }) { + const { activate, deactivate, localDevice } = usePushActivation(); + + useEffect(() => { + onDeviceChange(localDevice?.id ?? null); + }, [localDevice]); + + async function handleActivate() { + try { + await activate(); + onLog('Push activated. Device ID: ' + localDevice?.id); + } catch (error: unknown) { + onLog('Failed to activate push: ' + (error instanceof Error ? error.message : String(error))); + } + } + + async function handleDeactivate() { + try { + await deactivate(); + onLog('Push notifications deactivated.'); + } catch (error: unknown) { + onLog('Failed to deactivate push: ' + (error instanceof Error ? error.message : String(error))); + } + } + + const buttonStyle = { padding: '12px 20px', margin: '5px 0', width: '100%', display: 'block', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px', color: '#fff' }; + + return ( + <> + + + + ); +} +``` + + +`usePushActivation` returns: + +- **`activate`**: Registers the browser for push notifications. Requests notification permission, registers the service worker, and records the device with Ably. +- **`deactivate`**: Removes the device registration from Ably's servers. Call this only on explicit user opt-out. +- **`localDevice`**: The current `LocalDevice` if activated, `null` otherwise. Reactive — updates immediately when `activate` or `deactivate` is called, and is re-populated from `localStorage` on page load if the device was activated in a prior session. + +A service worker runs in the background and receives push notifications even when the page is not open. In Next.js, place the service worker in `public/` so it is served from the root path. + +Create `public/service-worker.js`: + + +```react +// Handle push events +self.addEventListener('push', (event) => { + const eventData = event.data.json(); + + // Prepare the notification object suitable for both `showNotification` and `postMessage` + const notification = { + title: eventData.notification.title, + body: eventData.notification.body, + data: eventData.data, + }; + + // Display a native browser notification + self.registration.showNotification(notification.title, notification); + + // Also forward to open pages (optional, for demonstration purposes) + event.waitUntil( + clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => { + clientList.forEach((client) => { + client.postMessage({ type: 'tutorial-push', notification }); + }); + }) + ); +}); +``` + + +Add a `notificationclick` listener in `public/service-worker.js` to handle what happens when the user clicks a notification: + + +```react +// Handle notification clicks +self.addEventListener('notificationclick', (event) => { + event.notification.close(); + + // Open or focus the app window + event.waitUntil( + clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => { + const url = event.notification.data?.url || '/push'; + + // Check if there's already a window open + for (const client of clientList) { + if ((client.url.endsWith('/push') || url === '/push') && client.focus) { + client.postMessage({ + type: 'tutorial-push-click', + notification: { + title: event.notification.title, + body: event.notification.body, + data: event.notification.data, + }, + }); + return client.focus(); + } + } + + // Open a new window if none exists + if (clients.openWindow) { + return clients.openWindow(url); + } + }) + ); +}); +``` + + +When a notification is clicked, the handler closes the notification, looks for an existing window, sends it the notification data via `postMessage`, and focuses it. If no window exists, it opens a new one. + +### Handle push notifications + +Add a `useEffect` to `PushApp.tsx` to receive messages forwarded from the service worker and write them to the log: + + +```react +import { useEffect, useState } from 'react'; + +// Inside PushApp, after the log function: +useEffect(() => { + if (!navigator.serviceWorker) return; + + const handler = (event: MessageEvent) => { + const notification = event.data?.notification; + if (!notification) return; + switch (event.data?.type) { + case 'tutorial-push': + log(`Received push: ${notification.title} — ${notification.body}, with data: ${JSON.stringify(notification.data)}`); + break; + case 'tutorial-push-click': + log(`Clicked push: ${notification.title} — ${notification.body}, with data: ${JSON.stringify(notification.data)}`); + break; + } + }; + + navigator.serviceWorker.addEventListener('message', handler); + return () => navigator.serviceWorker.removeEventListener('message', handler); +}, []); +``` + + +This listener is placed in `PushApp` rather than in a child component because push events are not channel-specific — a notification can arrive for any channel, or directly by device or client ID. + +## Step 3: Subscribe to channel push notifications + +Create `src/app/push/ChannelSubscription.tsx`. This component uses two hooks: + +- `usePush` to manage push subscriptions for the channel and expose `isActivated` +- `useChannel` to subscribe to realtime messages on the same channel + + +```react +'use client'; + +import { useChannel, usePush } from 'ably/react'; + +const CHANNEL_NAME = 'my-first-push-channel'; + +export function ChannelSubscription({ onLog }: { onLog: (msg: string) => void }) { + const { + subscribeDevice, + unsubscribeDevice, + isActivated, + connectionError, + channelError, + } = usePush(CHANNEL_NAME); + + // Subscribe to realtime messages on the channel + useChannel({ channelName: CHANNEL_NAME }, (message) => { + let logMessage = 'Received message: ' + message.name; + if (message.data) { + logMessage += '\n- data: ' + JSON.stringify(message.data); + } + if (message.extras?.push) { + logMessage += '\n- push: ' + message.extras.push.notification.title + + ' — ' + message.extras.push.notification.body; + } + onLog(logMessage); + }); + + async function handleSubscribe() { + try { + await subscribeDevice(); + onLog('Subscribed to push on channel: ' + CHANNEL_NAME); + } catch (error: unknown) { + onLog('Failed to subscribe: ' + (error instanceof Error ? error.message : String(error))); + } + } + + async function handleUnsubscribe() { + try { + await unsubscribeDevice(); + onLog('Unsubscribed from push on channel: ' + CHANNEL_NAME); + } catch (error: unknown) { + onLog('Failed to unsubscribe: ' + (error instanceof Error ? error.message : String(error))); + } + } + + if (connectionError) return

Connection error: {connectionError.message}

; + if (channelError) return

Channel error: {channelError.message}

; + + const buttonStyle = { padding: '12px 20px', margin: '5px 0', width: '100%', display: 'block', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '14px', color: '#fff' }; + + return ( + <> + + + + ); +} +``` +
+ +`usePush` returns `isActivated` — a reactive boolean shared with `usePushActivation` via a module-level store. When `PushActivationBanner` calls `activate()`, all `usePush` instances update automatically, so the subscribe buttons enable without any extra wiring. + +Run your app on a real device or compatible browser: + + +```shell +npm run dev +``` + + +Navigate to `http://localhost:3000/push` in your browser. + +## Step 4: Publish a push notification
+ +In the app click **Activate Push** and wait until the status message displays your device ID. + +### Publish directly to your device + +Publish a push notification directly to your client ID (or device ID using `--device-id` instead of `--client-id`) via the [Ably CLI](/docs/platform/tools/cli): + + +```shell +ably push publish --client-id push-tutorial-client \ + --title "Hello" \ + --body "World!" \ + --data '{"foo":"bar"}' +``` + + +### Publish via a channel + +Click **Subscribe to Channel** in the app, then publish a push notification to the channel using the [Ably CLI](/docs/platform/tools/cli): + + +```shell +ably push publish --channel my-first-push-channel \ + --title "Hello" \ + --body "World!" \ + --message '{"name":"greeting","data":"Hello World!"}' +``` + + +If you click **Unsubscribe from Channel**, the device no longer receives push notifications for that channel. Send the same command again and verify that no notification is received. + +To see the full list of options for sending push notifications with the Ably CLI, run `ably push publish --help` or see the [Ably CLI push documentation](/docs/cli/push). To send push notifications from your own server code instead of the CLI, see [Push notification publishing](https://ably.com/docs/push/publish). + +![Screenshot of the Next.js push tutorial application showing push notifications received](../../../../images/content/screenshots/getting-started/nextjs-push-getting-started-guide.png) + +## Browser compatibility + +Web Push notification support varies across browsers: + +| Feature | Chrome/Edge | Firefox | Safari | +|---|---|---|---| +| Push API | Full support | Full support | Partial (macOS 13+) | +| Service Worker | Full support | Full support | Full support | +| Notification actions (buttons) | Supported | Limited | Not supported | +| Silent push | Supported | Supported | Not supported | + + + +## Next steps + +* Understand [token authentication](/docs/auth/token) before going to production. +* Explore [push notification administration](/docs/push#push-admin) for managing devices and subscriptions. +* Learn about [channel rules](/docs/channels#rules) for channel-based push notifications. +* Read more about the [Push Admin API](/docs/api/realtime-sdk/push-admin). +* Check out the [Web Push Notifications](/docs/push/configure/web) documentation for advanced use cases. +* Explore [Ably CLI push commands](/docs/cli/push) for the full list of push CLI options. + +You can also explore the [Ably JavaScript SDK](https://github.com/ably/ably-js) on GitHub, or visit the [API references](/docs/api/realtime-sdk?lang=javascript) for additional functionality.