+```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).
+
+
+
+## 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.