npm package: @specno-marlon/convex-web-push
Description: Web Push notifications as a Convex component — subscribe, send, and track delivery with zero infrastructure.
Latest version: 0.1.1

# convex-web-push

Web push notifications for Convex apps. Send notifications to subscribers with a simple API, automatic subscription management, and built-in delivery tracking.

## Features

- **VAPID-secured push delivery** — industry-standard Web Push Protocol
- **Automatic subscription lifecycle** — handles permission requests, subscription rotation, and cleanup
- **Delivery tracking** — built-in stats, notification history, and subscriber management
- **Retry with exponential backoff** — auto-recovers from transient delivery failures
- **Dead subscription cleanup** — automatically removes 404/410 responses
- **React hook + service worker handlers** — zero-config notification UI and handlers

## Quick Start

### 1. Install package

```bash
npm install @specno-marlon/convex-web-push
# or: pnpm add @specno-marlon/convex-web-push
```

### 2. Generate VAPID keys

```bash
npx web-push generate-vapid-keys
```

Store the keys in your Convex environment variables:
```bash
# .env.local (or your deployment platform's env var settings)
CONVEX_VAPID_PUBLIC_KEY=<public key>
CONVEX_VAPID_PRIVATE_KEY=<private key>
```

### 3. Backend setup

In `convex/convex.config.ts`, add the component:

```typescript
import { defineProject } from "convex/server";
import webPush from "@specno-marlon/convex-web-push/convex.config";

export default defineProject({
  modules: [webPush],
});
```

Create `convex/push.ts` to export the client:

```typescript
import { WebPushClient } from "@specno-marlon/convex-web-push";

export const push = new WebPushClient();
```

### 4. Frontend setup

Register a service worker in your app entry point:

```typescript
// app/main.tsx or pages/_app.tsx
if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/sw.js");
}
```

Create `public/sw.js`:

```javascript
// public/sw.js — handles incoming push events
importScripts(
  "https://cdn.jsdelivr.net/npm/@specno-marlon/convex-web-push/dist/sw/bundle.js"
);

const { handlePush, handleClick, handleSubscriptionChange } = ConvexWebPush;

self.addEventListener("push", handlePush);
self.addEventListener("notificationclick", handleClick);
self.addEventListener("pushsubscriptionchange", handleSubscriptionChange);
```

### 5. Hook into React component

```typescript
import { usePushNotifications } from "@specno-marlon/convex-web-push/react";
import { push } from "./convex/api";

export function NotificationButton() {
  const { isSupported, isSubscribed, subscribe, unsubscribe, permission } =
    usePushNotifications(
      { subscribe: push.subscribe, unsubscribe: push.unsubscribe, updateSubscription: push.updateSubscription },
      {
        vapidPublicKey: import.meta.env.VITE_VAPID_PUBLIC_KEY,
        onNotification: (payload) => console.log("Push received:", payload),
        onNotificationClick: (payload) => console.log("Notification clicked:", payload),
      }
    );

  if (!isSupported) return <p>Push notifications not supported</p>;

  return (
    <button onClick={isSubscribed ? unsubscribe : subscribe}>
      {isSubscribed ? "Unsubscribe" : "Subscribe to Notifications"}
    </button>
  );
}
```

## API Reference

### WebPushClient

Backend API wrapper for sending notifications and managing subscribers.

```typescript
import { WebPushClient } from "@specno-marlon/convex-web-push";
const push = new WebPushClient();
```

| Method | Signature | Description |
|--------|-----------|-------------|
| `subscribe` | `(endpoint, keys_p256dh, keys_auth) => Promise<{subscriberId}>` | Register a new subscriber. Called by hook; rarely used directly. |
| `unsubscribe` | `(subscriberId) => Promise<void>` | Remove a subscriber. Called by hook on unsubscribe. |
| `updateSubscription` | `(subscriberId, endpoint, keys_p256dh, keys_auth) => Promise<void>` | Update keys after rotation. Called by hook; called automatically on subscription change. |
| `send` | `(title, body, url?, icon?, data?, subscriberIds?) => Promise<{notificationId, totalSent}>` | Broadcast to all subscribers or target specific ones. Delivery is async (scheduled). |
| `getStats` | `() => Promise<{totalSubscribers, notificationsSent, acceptanceRate}>` | Get aggregated delivery stats. |
| `getNotification` | `(notificationId) => Promise<Notification\|null>` | Retrieve a sent notification by ID. |
| `listSubscribers` | `(paginationOpts: PaginationOptions) => Promise<PaginationResult<Subscriber>>` | List active subscribers. Uses Convex cursor-based pagination. |

### usePushNotifications Hook

Frontend React hook for subscription management and notification callbacks.

```typescript
const {
  isSupported,      // boolean — device supports Web Push
  permission,       // NotificationPermission — "default" | "granted" | "denied"
  isSubscribed,     // boolean — user is currently subscribed
  subscriberId,     // string | null — unique subscriber ID
  subscribe,        // () => Promise<void> — request permission and subscribe
  unsubscribe,      // () => Promise<void> — unsubscribe and remove locally
} = usePushNotifications(api, options);
```

**Parameters:**

- `api`: Object with `subscribe`, `unsubscribe`, `updateSubscription` mutations from WebPushClient
- `options`:
  - `vapidPublicKey` (string, required) — public key for encryption
  - `onNotification` (optional) — callback when push arrives; payload is `{title, body, url?, icon?, data?}`
  - `onNotificationClick` (optional) — callback when user clicks notification

### Service Worker Handlers

Export from `convex-web-push/sw`; re-export in your service worker.

```typescript
import { handlePush, handleClick, handleSubscriptionChange } from "convex-web-push/sw";

self.addEventListener("push", handlePush);
self.addEventListener("notificationclick", handleClick);
self.addEventListener("pushsubscriptionchange", handleSubscriptionChange);
```

| Handler | Event | What it does |
|---------|-------|-------------|
| `handlePush` | `PushEvent` | Parse notification payload and display + notify open clients |
| `handleClick` | `NotificationEvent` | Close notification, open URL if present, notify clients |
| `handleSubscriptionChange` | `PushSubscriptionChangeEvent` | Notify clients that subscription rotated (hook handles update) |

## Environment Variables

Required for backend notification delivery:

| Variable | Source | Used for |
|----------|--------|----------|
| `VAPID_PUBLIC_KEY` | `npx web-push generate-vapid-keys` | Client-side subscription encryption; shared with frontend |
| `VAPID_PRIVATE_KEY` | `npx web-push generate-vapid-keys` | Backend signing of push messages; kept server-side only |
| `VAPID_SUBJECT` | e.g. `mailto:you@example.com` | Identifies the sender to push services; required by VAPID spec |

## How It Works

1. **Subscribe**: Browser requests notification permission, generates a push subscription via `PushManager`, and sends endpoint + keys to backend.
2. **Deliver**: Backend signs and sends a message to the subscription endpoint using the Web Push Protocol. Failed requests retry with exponential backoff.
3. **Notify**: When a push arrives, the service worker decrypts it, displays the notification, and posts a message to all open clients (for in-app callbacks).
4. **Track**: Every delivery attempt is logged. Dead subscriptions (404/410) are auto-cleaned. Stats API aggregates delivery success rate.

## License

MIT