@molecule/api-push-notifications
Core interface · push-notifications · API (Node) · v1.0.2 · Apache-2.0
Push notification provider interface (bond-compatible)
npm install @molecule/api-push-notificationsHow it works
@molecule/api-push-notifications is the push-notifications core interface on the API (Node) side: the API your app calls, with no vendor inside.
Choose the implementation by bonding one of its 3 providers: @molecule/api-push-capture, @molecule/api-push-notifications-molecule, @molecule/api-push-notifications-web-push.
import { configure, sendMany, getPublicKey } from '@molecule/api-push-notifications'
configure({ email: 'ops@app.com', publicKey: VAPID_PUBLIC, privateKey: VAPID_PRIVATE })
// Browser subscribes with the PUBLIC key only; the API stores the subscription per user.
router.post('/push/subscribe', async (req, res) => {
await saveSubscription(getUserId(res), req.body.subscription) // scoped to the user
res.json({ ok: true })
})
// Server sends, then prunes subscriptions the push service reports as gone.
// A dead endpoint REJECTS (it lands in r.error with a statusCode), it does not
// resolve into r.result — checking r.result for 404/410 would never prune anything.
const rows = await subscriptionsFor(userId)
const results = await sendMany(
rows.map((r) => r.subscription),
{ title: 'Hi', options: { body: '…' } },
)
results.forEach((r, i) => {
const gone = (r.error as { statusCode?: number } | undefined)?.statusCode
if (gone === 404 || gone === 410) deleteSubscription(rows[i].id)
})Providers (3): @molecule/api-push-capture, @molecule/api-push-notifications-molecule, @molecule/api-push-notifications-web-push
Works with: @molecule/api-bond
Reference
Push notification provider interface for molecule.dev.
Provides an abstract push notification interface that can be backed by any
push notification library. Use setProvider to bond a concrete implementation
such as @molecule/api-push-notifications-web-push.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { configure, sendMany, getPublicKey } from '@molecule/api-push-notifications'
configure({ email: 'ops@app.com', publicKey: VAPID_PUBLIC, privateKey: VAPID_PRIVATE })
// Browser subscribes with the PUBLIC key only; the API stores the subscription per user.
router.post('/push/subscribe', async (req, res) => {
await saveSubscription(getUserId(res), req.body.subscription) // scoped to the user
res.json({ ok: true })
})
// Server sends, then prunes subscriptions the push service reports as gone.
// A dead endpoint REJECTS (it lands in r.error with a statusCode), it does not
// resolve into r.result — checking r.result for 404/410 would never prune anything.
const rows = await subscriptionsFor(userId)
const results = await sendMany(
rows.map((r) => r.subscription),
{ title: 'Hi', options: { body: '…' } },
)
results.forEach((r, i) => {
const gone = (r.error as { statusCode?: number } | undefined)?.statusCode
if (gone === 404 || gone === 410) deleteSubscription(rows[i].id)
})
Type
core
Installation
npm install @molecule/api-push-notifications @molecule/api-bond
API
Interfaces
PushSubscription
A push subscription representing a client endpoint.
interface PushSubscription {
endpoint: string
keys: {
p256dh: string
auth: string
}
}
NotificationPayload
Notification payload structure.
interface NotificationPayload {
title: string
options?: {
body?: string
icon?: string
badge?: string
image?: string
tag?: string
data?: Record<string, unknown>
actions?: Array<{
action: string
title: string
icon?: string
}>
requireInteraction?: boolean
silent?: boolean
}
}
SendResult
Result of sending a single push notification.
interface SendResult {
statusCode: number
headers: Record<string, string>
body: string
}
SendManyResult
Result entry when sending to multiple subscriptions.
Exactly one of result / error is set per entry. A dead subscription
(HTTP 404/410 from the push service) is a FAILURE: it arrives in error
(provider errors such as web-push's WebPushError carry a statusCode),
never as a resolved result — prune stored subscriptions by inspecting
error, not result.
interface SendManyResult {
subscription: PushSubscription
result?: SendResult
error?: Error
}
VapidConfig
VAPID configuration for push notification authentication.
interface VapidConfig {
email: string
publicKey: string
privateKey: string
}
VapidKeys
VAPID key pair returned by key generation.
interface VapidKeys {
publicKey: string
privateKey: string
}
PushNotificationProvider
Push notification provider interface that all implementations must satisfy.
interface PushNotificationProvider {
/**
* Configure VAPID credentials for push notifications.
*/
configure(config?: VapidConfig): void
/**
* Sends a push notification to a single subscription endpoint.
*/
send(subscription: PushSubscription, payload: NotificationPayload): Promise<SendResult>
/**
* Sends a push notification to multiple subscriptions, handling
* partial failures gracefully.
*/
sendMany(
subscriptions: PushSubscription[],
payload: NotificationPayload,
): Promise<SendManyResult[]>
/**
* Generates a new VAPID key pair for push notification authentication.
*/
generateVapidKeys(): VapidKeys
/**
* Returns the public VAPID key for client-side subscription requests,
* or `undefined` if VAPID is not configured.
*/
getPublicKey(): string | undefined
}
Functions
setProvider(provider)
Registers a push notification provider as the active singleton. Called by bond packages during application startup.
function setProvider(provider: PushNotificationProvider): void
provider— The push notification provider implementation to bond.
getProvider()
Retrieves the bonded push notification provider, or undefined if none is bonded.
function getProvider(): PushNotificationProvider | undefined
Returns: The bonded push notification provider, or undefined.
hasProvider()
Checks whether a push notification provider is currently bonded.
function hasProvider(): boolean
Returns: true if a push notification provider is bonded.
requireProvider()
Retrieves the bonded push notification provider, throwing if none is configured.
function requireProvider(): PushNotificationProvider
Returns: The bonded push notification provider.
configure(config)
Configures VAPID credentials on the bonded push notification provider.
function configure(config?: VapidConfig): void
config— The VAPID configuration containing email, public key, and private key.
send(subscription, payload)
Sends a push notification to a single subscription endpoint.
function send(subscription: PushSubscription, payload: NotificationPayload): Promise<SendResult>
subscription— The push subscription containing the client endpoint and encryption keys.payload— The notification payload with title and optional display options.
Returns: The send result containing status code, headers, and response body.
sendMany(subscriptions, payload)
Sends a push notification to multiple subscription endpoints. Handles partial failures gracefully — each result includes either the send result or the error for that subscription.
function sendMany(
subscriptions: PushSubscription[],
payload: NotificationPayload,
): Promise<SendManyResult[]>
subscriptions— The push subscriptions to send to.payload— The notification payload with title and optional display options.
Returns: One result entry per subscription, each containing either result or error.
generateVapidKeys()
Generates a new VAPID key pair for push notification authentication.
function generateVapidKeys(): VapidKeys
Returns: An object containing publicKey and privateKey strings.
getPublicKey()
Returns the public VAPID key for client-side push subscription requests.
function getPublicKey(): string | undefined
Returns: The public VAPID key string, or undefined if not configured.
Available Providers
| Provider | Package |
|---|---|
| Capture | @molecule/api-push-capture |
| Molecule (hosted) | @molecule/api-push-notifications-molecule |
| Web Push (VAPID) | @molecule/api-push-notifications-web-push |
Injection Notes
Requirements
Peer dependencies:
@molecule/api-bond^1.0.1
Runtime Dependencies
-
@molecule/api-bond -
The VAPID private key is a server secret — it stays in the API (config/secrets). Only the public key (
getPublicKey) goes to the browser forsubscribe(). Generate the pair ONCE withgenerateVapidKeysand persist it in env; do NOT regenerate per boot — new keys invalidate every existing subscription. -
Store each
PushSubscriptionserver-side, scoped to its user, and send from the server withsend/sendMany. The browser never sends notifications and never holds another user's subscription. -
Prune dead endpoints: a gone subscription (HTTP 404/410 from the push service) surfaces as a failed send — with the web-push bond the per-subscription
errorinSendManyResult(aWebPushErrorcarryingstatusCode), NOT a resolvedSendResult. Checkr.errorfor 404/410 and delete that row so you stop pushing to it. (send()for a single subscription throws in that case.) -
configure()once at startup with yourVapidConfig(email + keys).
E2E Tests
Integration checklist — drive the real UI (live preview, no mocks), adapt each item to this app's actual screens/flows, and check every box off one by one. A box you can't check is an integration bug to fix — not a skip:
- The UI offers an enable-notifications control; activating it triggers the browser permission prompt and, once granted, the subscription is stored (the UI still shows "enabled" after a full reload).
- An event this app notifies about actually delivers a push to the
subscribed session, with a readable title/body (not raw JSON). The sandbox
CAPTURES outbound pushes instead of delivering — read the captured message
with the
read_activitytool (filter type 'push'); never mock the flow or modify production code to expose it. - Clicking the delivered notification opens/focuses the relevant screen (when the app claims deep-linking).
- Denying the permission leaves the app fully usable and truthful about the state (no crash, no false "enabled").
- Disabling/unsubscribing stops deliveries, and the disabled state persists across a reload.