npm package: @iron-stack/push
Description: Declarative push notification framework with multi-provider support (APNs, FCM, Expo), socket-aware delivery, and built-in throttling and batching.
Latest version: 1.0.0

# @iron-stack/push

Declarative push notification framework with multi-provider support (APNs, FCM, Expo), socket-aware delivery, and built-in throttling and batching.

## Installation

```bash
npm install @iron-stack/push
```

## Quick Start

### 1. Define push rules (shared)

```ts
import { definePushRules } from '@iron-stack/push';

const pushRules = definePushRules<{
  'message:new': MessageWithSender;
}>({
  'message:new': {
    shouldPush: (msg, recipient) => !recipient.isOnline,
    title: (msg) => msg.sender.displayName,
    body: (msg) => msg.content,
    deepLink: (msg) => `myapp://conversation/${msg.conversationId}`,
    collapseKey: (msg) => `conv:${msg.conversationId}`,
    threadId: (msg) => msg.conversationId,
    throttleMs: 2000,
    batchWindowMs: 3000,
    batchTitle: (events) => events[0].sender.displayName,
    batchBody: (_, count) => `${count} new messages`,
  },
});
```

### 2. Server -- send push notifications

```ts
import { PushService, createExpoProvider, createSocketTracker } from '@iron-stack/push/server';

const push = new PushService({
  registry: yourDeviceRegistry,
  onlineTracker: createSocketTracker(),
  rules: pushRules,
  providers: [createExpoProvider()],
});

// Trigger push for affected users
await push.notify('message:new', messageData, recipientUserIds);
```

### 3. Client -- configure push (type only)

```ts
import type { PushClientConfig } from '@iron-stack/push/client';

const pushConfig: PushClientConfig = {
  platform: 'ios',
  registerForToken: async () => { /* get push token from OS */ },
  requestPermission: async () => { /* request notification permission */ },
  sendTokenToServer: async (token) => { /* register token with server */ },
};
```

## API Reference

### `@iron-stack/push` (shared)

| Export | Description |
|---|---|
| `definePushRules(rules)` | Type-safe factory for declaring per-event push behavior |
| `PlatformSchema` | Zod schema for platform enum (`ios`, `android`, `web`) |
| `PushTokenSchema` | Zod schema for push token objects |
| `Platform` | Type: `"ios" \| "android" \| "web"` |
| `PushToken` | Type for stored device tokens |
| `NotificationPayload` | Type for the notification content (title, body, data, badge, sound, etc.) |
| `PushResult` | Type for send results per token |
| `PushRule` | Type for a single push rule definition |
| `PushRules` | Type for the complete rules map |
| `PushRecipient` | Type for resolved recipient (userId, isOnline, tokens) |
| `PushProvider` | Interface that push providers must implement |
| `DeviceRegistry` | Interface for device token storage (register, remove, query, prune) |
| `OnlineTracker` | Interface for checking user online status |

### `@iron-stack/push/server`

| Export | Description |
|---|---|
| `PushService` | Main push engine -- socket-aware, declarative, multi-provider |
| `PushService.notify(event, data, recipientIds)` | Trigger push for an event based on rules |
| `PushService.sendDirect(userId, payload)` | Send a push directly, bypassing rules |
| `PushService.addProvider(provider)` | Register an additional push provider |
| `PushService.pruneStaleTokens(days)` | Remove tokens unused for N days |
| `PushService.flushAll()` | Flush all pending batched notifications |
| `PushQueue` | Queue with built-in throttling and batching |
| `createSocketTracker()` | Creates an in-memory online tracker backed by Socket.IO |
| `createMemoryRegistry()` | In-memory device registry for development and testing |
| `createAPNsProvider(config)` | Apple Push Notification Service provider (HTTP/2) |
| `createFCMProvider(config)` | Firebase Cloud Messaging v1 API provider |
| `createExpoProvider(config?)` | Expo push notification provider |
| `PushServiceConfig` | Configuration for `PushService` |
| `APNsConfig` | Configuration for APNs provider |
| `FCMConfig` | Configuration for FCM provider |
| `ExpoConfig` | Configuration for Expo provider |

### `@iron-stack/push/client`

| Export | Description |
|---|---|
| `PushClientConfig` | Interface for platform-agnostic client push configuration |
| `NotificationHandler` | Interface with `onForeground`, `onTap`, `onBackground` callbacks |
| `PushPermissionStatus` | Type: `"granted" \| "denied" \| "undetermined"` |

## Configuration

### `PushServiceConfig`

| Option | Type | Default | Description |
|---|---|---|---|
| `registry` | `DeviceRegistry` | **required** | Device token storage backend |
| `onlineTracker` | `OnlineTracker` | `undefined` | Socket-based online status (skip pushes for online users) |
| `rules` | `PushRules` | **required** | Declarative per-event push config |
| `providers` | `PushProvider[]` | `[]` | Registered push providers (APNs, FCM, Expo) |
| `autoRemoveInvalidTokens` | `boolean` | `true` | Auto-remove expired/invalid tokens |
| `verbose` | `boolean` | `true` | Log push events to console |

### `PushRule` Options

| Option | Type | Description |
|---|---|---|
| `shouldPush` | `(event, recipient) => boolean` | Skip push when returning `false` (e.g., user is online) |
| `title` | `string \| (event) => string` | Notification title |
| `body` | `string \| (event) => string` | Notification body |
| `data` | `(event) => Record<string, string>` | Custom data payload |
| `deepLink` | `(event) => string` | Deep link URL opened on tap |
| `collapseKey` | `(event) => string` | Newer pushes with same key replace older ones |
| `threadId` | `(event) => string` | Group notifications by thread |
| `imageUrl` | `(event) => string` | Rich notification image |
| `priority` | `"normal" \| "high"` | Push priority (default: `"high"`) |
| `silent` | `boolean` | Silent/background push |
| `throttleMs` | `number` | Min ms between pushes per recipient + collapse key |
| `batchWindowMs` | `number` | Combine events within this window into one push |
| `batchTitle` | `(events, count) => string` | Title for batched notifications |
| `batchBody` | `(events, count) => string` | Body for batched notifications |

### Provider Configs

#### `APNsConfig`

| Option | Type | Default | Description |
|---|---|---|---|
| `key` | `string` | **required** | `.p8` key file contents |
| `keyId` | `string` | **required** | Key ID from Apple Developer portal |
| `teamId` | `string` | **required** | Team ID from Apple Developer portal |
| `bundleId` | `string` | **required** | App bundle identifier |
| `production` | `boolean` | `false` | Use production APNs endpoint |

#### `FCMConfig`

| Option | Type | Description |
|---|---|---|
| `projectId` | `string` | Firebase project ID |
| `serviceAccountKey` | `string` | Service account JSON string |

#### `ExpoConfig`

| Option | Type | Description |
|---|---|---|
| `accessToken` | `string?` | Expo access token (optional, for higher rate limits) |

## License

MIT