diff --git a/docs/react-push.md b/docs/react-push.md
new file mode 100644
index 0000000000..4f29d55df4
--- /dev/null
+++ b/docs/react-push.md
@@ -0,0 +1,401 @@
+# React Hooks for Push Notifications
+
+Use Ably Push Notifications in your React application using idiomatic React Hooks.
+
+Using these hooks you can:
+
+- [Activate and deactivate devices](https://ably.com/docs/push/activate-subscribe) for push notifications
+- [Subscribe devices or clients](https://ably.com/docs/push/activate-subscribe#subscribing) to push notifications on channels
+- List active push subscriptions for a channel
+
+> [!NOTE]
+> Push notifications require the Push plugin to be loaded. If you're using the modular bundle, ensure the Push plugin is included in your client options. See the [Push Notifications documentation](https://ably.com/docs/push) for general concepts and setup.
+
+---
+
+
+
+
+- [Prerequisites](#prerequisites)
+- [usePushActivation](#usepushactivation)
+- [usePush](#usepush)
+- [Error Handling](#error-handling)
+- [Full Example](#full-example)
+- [API Reference](#api-reference)
+
+##
+
+## Prerequisites
+
+Push hooks require the Ably client to be configured with the Push plugin. When using the default `ably` bundle, the Push plugin is included automatically. If you're using the modular bundle, you must provide it explicitly:
+
+```jsx
+import * as Ably from 'ably';
+import Push from 'ably/push';
+
+const client = new Ably.Realtime({
+ key: 'your-ably-api-key',
+ clientId: 'me',
+ plugins: { Push },
+});
+
+root.render(
+
+
+ ,
+);
+```
+
+---
+
+## usePushActivation
+
+The `usePushActivation` hook provides functions to activate and deactivate the current device for push notifications. It works directly under an `AblyProvider` and does **not** require a `ChannelProvider`.
+
+```jsx
+import { usePushActivation } from 'ably/react';
+
+const PushActivationComponent = () => {
+ const { activate, deactivate, localDevice } = usePushActivation();
+
+ return (
+
+ );
+};
+```
+
+The `localDevice` property is reactive — it updates when `activate()` or `deactivate()` is called. It is also initialised from `localStorage` on mount, so if the device was activated in a prior session, `localDevice` will be populated immediately.
+
+#### Activation lifecycle
+
+Activation registers the device with Ably's push service (on web, this requests browser notification permission and registers a service worker). The device identity is persisted to `localStorage`, so:
+
+- **Activation survives page reloads and app restarts.** You do not need to call `activate()` on every mount.
+- **Calling `activate()` when already activated is safe** — it confirms the existing registration without side effects.
+- **`deactivate()` is for explicit user opt-out only.** It removes the device registration from Ably's servers and clears all persisted push state. Do not call it on unmount or app close.
+
+A typical pattern is to call `activate()` once in response to a user action (e.g. tapping "Enable notifications"), not automatically on mount:
+
+```jsx
+const NotificationBanner = () => {
+ const { activate, localDevice } = usePushActivation();
+
+ const handleEnable = async () => {
+ try {
+ await activate();
+ } catch (err) {
+ console.error('Push activation failed:', err);
+ }
+ };
+
+ if (localDevice) return null;
+
+ return (
+
+
Get notified about new updates
+
+
+ );
+};
+```
+
+#### Multiple clients
+
+If you use multiple Ably clients via the `ablyId` pattern, pass the ID to `usePushActivation`:
+
+```jsx
+const { activate, deactivate } = usePushActivation('providerOne');
+```
+
+---
+
+## usePush
+
+The `usePush` hook provides functions to manage push notification subscriptions for a specific channel. It must be used inside a `ChannelProvider`.
+
+```jsx
+import { usePush } from 'ably/react';
+
+const PushSubscriptionComponent = () => {
+ const { subscribeDevice, unsubscribeDevice, isActivated } = usePush('your-channel-name');
+
+ return (
+
+
+
+ {!isActivated &&
Push must be activated before subscribing.
}
+
+ );
+};
+```
+
+#### Activation awareness
+
+`usePush` is aware of whether push has been activated via `usePushActivation`. The `isActivated` property is reactive — when `usePushActivation` calls `activate()` or `deactivate()`, all `usePush` instances update automatically, even if they are in different components. This works via a shared store without requiring any additional providers.
+
+> [!IMPORTANT]
+> The device must be activated (via `usePushActivation`) before calling `subscribeDevice()` or `unsubscribeDevice()`. Use `isActivated` to guard your UI or check before calling. See [Error Handling](#error-handling) for details on what happens if activation hasn't been completed.
+
+#### Subscribe by device or by client
+
+`usePush` supports both device-level and client-level subscriptions:
+
+```jsx
+const {
+ subscribeDevice, // Subscribe the current device
+ unsubscribeDevice, // Unsubscribe the current device
+ subscribeClient, // Subscribe all devices for the current clientId
+ unsubscribeClient, // Unsubscribe all devices for the current clientId
+} = usePush('your-channel-name');
+```
+
+- **Device subscriptions** target the specific device. Use when you want per-device control.
+- **Client subscriptions** target all devices that share the same `clientId`. Use when a user should receive push notifications regardless of which device they're on.
+
+> [!NOTE]
+> `subscribeClient` and `unsubscribeClient` require the Ably client to be configured with a `clientId`. An error will be thrown if no `clientId` is set.
+
+#### Listing subscriptions
+
+You can list active push subscriptions for the channel:
+
+```jsx
+const { listSubscriptions } = usePush('your-channel-name');
+
+const handleListSubscriptions = async () => {
+ const result = await listSubscriptions();
+ console.log('Active subscriptions:', result.items);
+};
+```
+
+`listSubscriptions` accepts an optional params object to filter by `deviceId` or `clientId`:
+
+```jsx
+const result = await listSubscriptions({ deviceId: 'specific-device-id' });
+```
+
+#### Push subscriptions are persistent
+
+Unlike presence (which enters on mount and leaves on unmount), push subscriptions are **persistent server-side state**. They survive app restarts and are not automatically removed when a component unmounts. This is by design — push notifications are meant to be delivered even when your app is not running.
+
+To remove a subscription, explicitly call `unsubscribeDevice()` or `unsubscribeClient()` in response to a user action.
+
+---
+
+## Error Handling
+
+### Push plugin not loaded
+
+If the Push plugin is not included in your client configuration, `usePush` will throw immediately on render:
+
+```
+Error: Push plugin not provided (code: 40019)
+```
+
+For `usePushActivation`, the error is thrown when `activate()` or `deactivate()` is called.
+
+To fix this, ensure the Push plugin is loaded. See [Prerequisites](#prerequisites).
+
+### Device not activated
+
+If you call `subscribeDevice()` or `unsubscribeDevice()` before the device has been activated, the promise will reject with:
+
+```
+Error: Cannot subscribe from client without deviceIdentityToken (code: 50000)
+```
+
+The recommended way to prevent this is to use the `isActivated` flag from `usePush` to guard your UI:
+
+```jsx
+const { subscribeDevice, isActivated } = usePush('alerts');
+
+// Disable the button until push is activated
+
+```
+
+Alternatively, you can sequence activation and subscription imperatively:
+
+```jsx
+const { activate } = usePushActivation();
+const { subscribeDevice } = usePush('alerts');
+
+const handleEnablePush = async () => {
+ await activate();
+ await subscribeDevice();
+};
+```
+
+### No clientId set
+
+If you call `subscribeClient()` or `unsubscribeClient()` without a `clientId` configured on the Ably client, the promise will reject with:
+
+```
+Error: Cannot subscribe from client without client ID (code: 50000)
+```
+
+Ensure your Ably client is created with a `clientId`:
+
+```jsx
+const client = new Ably.Realtime({ key: 'your-api-key', clientId: 'me' });
+```
+
+### Connection and channel errors
+
+Like other channel-level hooks, `usePush` returns `connectionError` and `channelError`:
+
+```jsx
+const { subscribeDevice, connectionError, channelError } = usePush('your-channel-name');
+
+if (connectionError) {
+ return