> ## Documentation Index
> Fetch the complete documentation index at: https://replyke-feat-push-rich-payload-fields.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Push Notifications

> Deliver native OS push notifications to your users on iOS, Android, and Web — from a single server-side call.

Push Notifications lets your app deliver native OS push notifications to users on iOS (via APNs), Android (via FCM), and the browser (via Web Push) — all from a single server-side call. Sublay stores each user's registered devices, fans a message out to every platform, and prunes stale tokens automatically.

<Note>
  **Requires the `push` bundle.** Install it from your project's **Database** page in the [dashboard](https://dash.sublay.io) before using any push feature. See [Bundles](/bundles) for details.
</Note>

## How It Works

1. **Configure credentials** in the dashboard — paste your APNs `.p8` key, FCM service account JSON, or enable Web Push (keypair generated server-side).
2. **Register devices** — your client app calls `register()` from `usePushRegistration` at a moment that makes sense for your UX (settings screen, first-run prompt, etc.). The SDK handles permission, token retrieval, and server registration.
3. **Send notifications** — your backend calls `sublay.push.send({ userIds, title, body, data })`. Sublay fans the message out to every registered device for those users across all platforms.

## Dashboard Setup

### Installing the bundle

Open **Database → Bundles** in the [dashboard](https://dash.sublay.io) and install `push`. Once provisioning completes, the **Push Notifications** settings page becomes active.

### Configuring providers

Go to **Settings → Push Notifications** in the dashboard. Configure each platform you want to support:

**APNs (iOS)**

| Field      | Description                                                             |
| ---------- | ----------------------------------------------------------------------- |
| Key ID     | The 10-character key identifier from your Apple Developer account       |
| Team ID    | Your Apple Developer team ID                                            |
| Bundle ID  | Your app's bundle identifier (e.g. `com.example.myapp`)                 |
| .p8 key    | Contents of the `.p8` private key file downloaded from Apple            |
| Production | Toggle for sandbox vs. production APNs gateway (defaults to production) |

**FCM (Android)**

Upload or paste your Firebase service account JSON. This is the file downloaded from the Firebase console under **Project Settings → Service Accounts → Generate new private key**.

**Web Push**

No credentials to paste — click **Enable**. Sublay generates a VAPID EC keypair server-side and stores the private key encrypted. Only the public key is returned and displayed; it is also available via the unauthenticated [`GET /vapid-public-key`](/api-reference/push-notifications/get-vapid-public-key) endpoint for use in your service worker.

### Test-send UI

Each provider card in the dashboard includes a **Send Test Push** panel. Supply a platform, a device token or subscription, a title, and a body, then click **Send Test Push** to verify credentials before shipping.

## Client SDK — Registering Devices

Use `usePushRegistration` with the adapter for your platform. Call `register()` in response to a deliberate user action — not on mount — because requesting OS push permission is a one-shot prompt that users cannot undo.

```tsx theme={null}
// Expo
import { usePushRegistration } from "@sublay/core";
import { expoPushTokenAdapter } from "@sublay/expo";

function SettingsScreen() {
  const { register, registering } = usePushRegistration(expoPushTokenAdapter);
  return <Button onPress={register} loading={registering} title="Enable notifications" />;
}
```

```tsx theme={null}
// Bare React Native
import { usePushRegistration } from "@sublay/core";
import { reactNativePushTokenAdapter } from "@sublay/react-native";

const { register } = usePushRegistration(reactNativePushTokenAdapter);
```

```tsx theme={null}
// Web (browser)
import { usePushRegistration } from "@sublay/core";
import { webPushTokenAdapter } from "@sublay/react-js";

const { register, unregister } = usePushRegistration(webPushTokenAdapter);
```

The web adapter requires a service worker. Register one before calling `register()`. See [SDK Reference → Push Notifications](/sdk/push-notifications/overview) for per-platform setup and [`usePushRegistration`](/hooks/push/use-push-registration) for the full hook API.

## Server SDK — Sending Notifications

Call `client.push.send()` from your backend whenever you want to notify users:

```typescript theme={null}
import { SublayClient } from "@sublay/node";

const sublay = await SublayClient.init({
  projectId: process.env.SUBLAY_PROJECT_ID!,
  apiKey: process.env.SUBLAY_SERVICE_KEY!,
});

const result = await sublay.push.send({
  userIds: ["usr_abc123", "usr_def456"],
  title: "You have a new match!",
  body: "Tap to see who liked your post.",
  data: { screen: "matches" },
});

for (const [userId, devices] of Object.entries(result.results)) {
  if (devices.length === 0) {
    console.log(`${userId} has no registered devices`);
  } else {
    console.log(`${userId}:`, devices);
  }
}
```

A single call fans the notification out to all platforms. Capped at **100 user IDs per request**. See [Node SDK — Push Notifications](/node-sdk/push-notifications) for the full module reference.

<Tip>
  A common pattern is to bridge Sublay's in-app notifications to push: subscribe to the `notification.created` webhook and call `push.send()` when it fires. See [Webhooks → Push Notification Bridge](/webhooks#push-notification-bridge).
</Tip>

## Rich Notification Payloads

Beyond `title`, `body`, and `data`, `push.send()` accepts optional fields for sound, badges, images, grouping, priority, and lifetime. Each maps to the native APNs / FCM / Web Push capability and is **silently ignored on platforms that don't support it** — so you can set `subtitle` (iOS-only) and `channelId` (Android-only) in the same call without branching.

```typescript theme={null}
await sublay.push.send({
  userIds: ["usr_abc123"],
  title: "New message",
  body: "Alice sent you a message",
  data: { conversationId: "conv_xyz789" },
  sound: "notification.wav",   // custom sound
  channelId: "messages",       // Android channel (see below)
  badge: 3,                    // iOS app-icon badge
  tag: "conv_xyz789",          // collapse a conversation into one notification
  priority: "high",
});
```

| Field            | Platforms         | Notes                                                                                      |
| ---------------- | ----------------- | ------------------------------------------------------------------------------------------ |
| `sound`          | iOS, Android, Web | Filename of a bundled sound. On Android 8+ the **channel** owns the sound — see below.     |
| `badge`          | iOS               | App-icon badge count. Your backend supplies the number; Sublay tracks no unread state.     |
| `channelId`      | Android           | Notification channel id. Created client-side.                                              |
| `priority`       | iOS, Android      | `"high"` (default) wakes the device; `"normal"` is power-considerate.                      |
| `subtitle`       | iOS               | Line under the title.                                                                      |
| `imageUrl`       | iOS, Android, Web | Big-picture image. iOS needs a Notification Service Extension — see below.                 |
| `tag`            | Android, Web      | Display-replace key so notifications collapse instead of stacking.                         |
| `collapseId`     | iOS, Android      | Transport-level collapse — a newer push supersedes an undelivered one.                     |
| `threadId`       | iOS               | Notification grouping.                                                                     |
| `ttl`            | iOS, Android      | Time-to-live in **seconds** for offline devices.                                           |
| `mutableContent` | iOS               | Enables your Notification Service Extension. Set automatically when `imageUrl` is present. |

### Client-side requirements

Three of these need setup in **your app** — Sublay forwards the field but cannot do this part for you:

<AccordionGroup>
  <Accordion title="Android sound → notification channels">
    On Android 8+ the **notification channel** owns the sound, importance, and vibration — the payload can't override it. Create the channel once in your app (with the bundled sound) and pass its id as `channelId`:

    ```ts theme={null}
    import * as Notifications from "expo-notifications";

    await Notifications.setNotificationChannelAsync("messages", {
      name: "Messages",
      importance: Notifications.AndroidImportance.HIGH,
      sound: "notification.wav", // bundled in the app
    });
    ```

    Then `push.send({ ..., channelId: "messages", sound: "notification.wav" })`. The `sound` field alone is only a pre-Android-8 fallback. Sublay does **not** create channels for you.
  </Accordion>

  <Accordion title="iOS images → Notification Service Extension">
    iOS does not download remote images from the payload on its own. To show `imageUrl` on iOS, add a [Notification Service Extension](https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension) to your app that reads the URL from the notification payload, downloads it, and attaches it. Sublay sets `mutable-content` automatically whenever `imageUrl` is present so the extension is allowed to run. Android and Web render `imageUrl` with no extra work.
  </Accordion>

  <Accordion title="Web rendering → your service worker">
    Sublay ships no service worker — your app's own SW renders web notifications. The server forwards the web-renderable fields (`sound`, `image`, `tag`) in the push JSON; your `push` event handler decides how to display them via `registration.showNotification(...)`.
  </Accordion>
</AccordionGroup>

## Device Lifecycle

* **Re-registration:** registering the same physical device again (same token or endpoint) updates the existing record instead of duplicating it.
* **Device reassignment:** if the same device is registered by a different user (e.g. a shared device after logout/login), the record is reassigned to the new user.
* **Stale token cleanup:** tokens or subscriptions permanently rejected by APNs, FCM, or Web Push during a send are automatically deleted — no separate cleanup pass is needed.
* **Explicit logout:** call `unregister()` in your logout flow so the device stops receiving notifications after sign-out.

## References

Everything related to push notifications across the docs:

<CardGroup cols={2}>
  <Card title="SDK Reference — Push Notifications" icon="mobile-screen-button" href="/sdk/push-notifications/overview">
    Per-platform client setup for Expo, React Native, and Web
  </Card>

  <Card title="usePushRegistration" icon="cube" href="/hooks/push/use-push-registration">
    Request permission, register, and unregister the current user's device
  </Card>

  <Card title="Node SDK — Push Notifications" icon="node-js" href="/node-sdk/push-notifications">
    The `push.send()` server module reference
  </Card>

  <Card title="Webhooks — Push Bridge" icon="webhook" href="/webhooks#push-notification-bridge">
    Forward `notification.created` events to push
  </Card>
</CardGroup>

### API Endpoints

<CardGroup cols={2}>
  <Card title="Register Device" href="/api-reference/push-notifications/register-device">
    `POST /push-notifications/devices`
  </Card>

  <Card title="Deregister Device" href="/api-reference/push-notifications/deregister-device">
    `DELETE /push-notifications/devices`
  </Card>

  <Card title="Send Push" href="/api-reference/push-notifications/send">
    `POST /push-notifications/send`
  </Card>

  <Card title="Get VAPID Public Key" href="/api-reference/push-notifications/get-vapid-public-key">
    `GET /push-notifications/vapid-public-key`
  </Card>
</CardGroup>
