@molecule/api-push-notifications-molecule
Provider bond · push-notifications · API (Node) · v1.1.1 · Apache-2.0
molecule.dev hosted push notifications provider — web push delivered through your molecule project, VAPID handled for you
npm install @molecule/api-push-notifications-moleculenpm · Source on GitHub · Implements @molecule/api-push-notifications
How it works
@molecule/api-push-notifications-molecule is a provider bond on the API (Node) side: it implements the push-notifications core interface (@molecule/api-push-notifications) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
Works with: @molecule/api-push-notifications, @molecule/api-secrets
Secrets: MOLECULE_API_KEY
Reference
molecule.dev hosted push notifications provider — web push delivered through your molecule project, VAPID handled for you
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.
Type
provider
Installation
npm install @molecule/api-push-notifications-molecule @molecule/api-push-notifications @molecule/api-secrets
API
Interfaces
MoleculePushConfig
Options for createProvider. Every field falls back to an env var,
so the zero-argument provider export works from .env alone.
interface MoleculePushConfig {
/**
* The molecule project API key (`mk_…`), or the sandbox token molecule.dev
* writes for in-IDE apps (`mbk_…`). Defaults to `MOLECULE_API_KEY`.
*/
apiKey?: string
/**
* The hosted services base URL, without a trailing slash. Defaults to
* `MOLECULE_SERVICES_URL`, then `https://api.molecule.dev/api/v1/services`.
*/
servicesUrl?: string
/** Per-request timeout in milliseconds. Default 15000. */
timeoutMs?: number
}
ProcessEnv
Environment variables this provider reads.
interface ProcessEnv {
MOLECULE_API_KEY?: string
MOLECULE_SERVICES_URL?: string
}
Classes
MoleculeServiceError
Error thrown for a refused or failed hosted-service call.
class MoleculeServiceError extends Error {
/** HTTP status from molecule.dev. */
readonly status: number
/** Stable error key from molecule.dev, e.g. `broker.error.budgetExceeded`. */
readonly errorKey: string | undefined
/**
* Create the error.
*
* @param message - Human-readable message.
* @param status - HTTP status.
* @param errorKey - Stable error key, when the service sent one.
* @param cause - The underlying error, if any.
*/
constructor(message: string, status: number, errorKey?: string, cause?: unknown) {
super(message, cause === undefined ? undefined : { cause })
this.name = 'MoleculeServiceError'
this.status = status
this.errorKey = errorKey
}
}
MoleculePushProvider
Push provider backed by molecule.dev's hosted service.
class MoleculePushProvider implements PushNotificationProvider {
readonly name = 'molecule'
private readonly apiKey: string
private readonly servicesUrl: string
private readonly timeoutMs: number
private cachedPublicKey: string | undefined
/**
* Create the provider.
*
* @param config - Options; each falls back to its env var.
*/
constructor(config: MoleculePushConfig = {}) {
this.apiKey = config.apiKey ?? process.env.MOLECULE_API_KEY ?? ''
this.servicesUrl = resolveServicesUrl(
config.servicesUrl ?? process.env.MOLECULE_SERVICES_URL ?? DEFAULT_SERVICES_URL,
)
this.timeoutMs = config.timeoutMs ?? 15_000
}
/**
* Nothing to configure locally — molecule.dev holds the VAPID keys and
* signs every send. The parameter is accepted so the provider satisfies
* the core interface; a self-hosted setup should use
* `@molecule/api-push-notifications-web-push` instead.
*
* @param _config - Ignored.
*/
configure(_config?: VapidConfig): void {}
/**
* Sends a push notification to a subscription endpoint.
*
* @param subscription - The push subscription (endpoint + keys).
* @param payload - The notification payload (title + display options).
* @returns The push service's answer: `statusCode` 201 means delivered;
* 404/410 mean the subscription is dead and should be pruned.
*/
async send(subscription: PushSubscription, payload: NotificationPayload): Promise<SendResult> {
assertValidSubscription(subscription)
assertValidPayload(payload)
return this.request<SendResult>('send', { subscription, payload })
}
/**
* Sends a push notification to multiple subscriptions. One refused or
* failed subscription never aborts the batch — each entry carries either
* `result` or `error` (a dead subscription arrives as a `result` whose
* `statusCode` is 404/410; prune on it).
*
* @param subscriptions - The push subscriptions to send to.
* @param payload - The notification payload with title and optional display options.
* @returns One result entry per subscription.
*/
async sendMany(
subscriptions: PushSubscription[],
payload: NotificationPayload,
): Promise<SendManyResult[]> {
const settled = await Promise.allSettled(
(subscriptions ?? []).map((subscription) => this.send(subscription, payload)),
)
return settled.map((outcome, i) =>
outcome.status === 'fulfilled'
? { subscription: subscriptions[i], result: outcome.value }
: { subscription: subscriptions[i], error: outcome.reason as Error },
)
}
/**
* Returns molecule.dev's public VAPID key — the `applicationServerKey` a
* browser's `pushManager.subscribe()` must be given for the resulting
* subscriptions to be sendable through molecule.dev.
*
* The core's method is SYNCHRONOUS while the key lives behind a service
* call, so this returns the cache: `undefined` until `fetchPublicKey`
* has run (or a send cached it). Await the fetch first:
*
* ```typescript
* await provider.fetchPublicKey() // once, e.g. at startup
* const applicationServerKey = provider.getPublicKey()
* ```
*
* @returns The cached public key, or `undefined` before the first fetch.
*/
getPublicKey(): string | undefined {
return this.cachedPublicKey
}
/**
* Fetches molecule.dev's public VAPID key from the hosted service and
* caches it for `getPublicKey`.
*
* @returns The public key string.
*/
async fetchPublicKey(): Promise<string> {
const { publicKey } = await this.request<{ publicKey: string }>('vapid-key', {})
this.cachedPublicKey = publicKey
return publicKey
}
/**
* Not supported on the hosted path: sends are signed with molecule.dev's
* VAPID identity, and subscriptions are bound to the key they were created
* with — a locally generated pair could never receive a hosted send. Use
* `@molecule/api-push-notifications-web-push` (which serves your own keys)
* when you need your own identity.
*
* @throws {MoleculeServiceError} Always, with the self-hosted pointer.
*/
generateVapidKeys(): never {
throw new MoleculeServiceError(
"generateVapidKeys() is a self-hosted concern: the hosted service signs sends with molecule.dev's VAPID identity. Use @molecule/api-push-notifications-web-push to serve your own keys.",
400,
'hostedServices.error.invalidInput',
)
}
/** One POST to the hosted service. */
private async request<T>(
operation: 'send' | 'vapid-key',
body: Record<string, unknown>,
): Promise<T> {
if (!this.apiKey) {
throw new MoleculeServiceError(
'MOLECULE_API_KEY is not set. Create a project API key on molecule.dev (or with `mlcl apikey create`) and put it in the API .env.',
401,
'hostedServices.error.tokenInvalid',
)
}
const response = await fetch(`${this.servicesUrl}/push/${operation}`, {
method: 'POST',
headers: { authorization: `Bearer ${this.apiKey}`, 'content-type': 'application/json' },
body: JSON.stringify(body),
signal: AbortSignal.timeout(this.timeoutMs),
})
const text = await response.text()
let data: unknown
try {
data = text ? JSON.parse(text) : {}
} catch (error) {
// A proxy/gateway error page, not the service: report the status only.
throw new MoleculeServiceError(
`molecule.dev returned ${response.status} with a non-JSON body.`,
response.status,
undefined,
error,
)
}
if (!response.ok) {
const err = data as { error?: string; errorKey?: string }
const hint = HINTS[response.status]
throw new MoleculeServiceError(
`molecule.dev push/${operation} failed (${response.status}): ${err.error ?? 'error'}${hint ? ` ${hint}` : ''}`,
response.status,
err.errorKey,
)
}
return data as T
}
}
Functions
createProvider(config)
Create a hosted push notifications provider.
function createProvider(config?: MoleculePushConfig): PushNotificationProvider
config— Options; each falls back to its env var.
Returns: A PushNotificationProvider backed by molecule.dev.
Constants
provider
The provider implementation (wire with setProvider).
const provider: PushNotificationProvider
DEFAULT_SERVICES_URL
Default hosted services base URL.
const DEFAULT_SERVICES_URL: 'https://api.molecule.dev/api/v1/services'
PUSH_SERVICE_LIMITS
Per-request limits of the hosted service.
const PUSH_SERVICE_LIMITS: {
readonly maxPayloadChars: 3072
readonly maxKeyChars: 512
readonly maxEndpointChars: 2048
}
pushNotificationsMoleculeSecretDefinitions
Secret definitions required by the molecule.dev hosted push bond.
const pushNotificationsMoleculeSecretDefinitions: SecretDefinition[]
Core Interface
Implements @molecule/api-push-notifications interface.
Bond Wiring
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/api-push-notifications'
import { provider } from '@molecule/api-push-notifications-molecule'
export function setupPushNotificationsMolecule(): void {
setProvider(provider)
}
Injection Notes
Requirements
Peer dependencies:
@molecule/api-push-notifications>=1.0.2@molecule/api-secrets^1.0.1
Environment Variables
MOLECULE_API_KEY(required) — molecule.dev project API key- Setup: Create one for your project with
mlcl apikey create --project <id> --name <name>(keys start with mk_). - Get it here: https://www.molecule.dev
- Example:
mk_...
- Setup: Create one for your project with
Runtime Dependencies
@molecule/api-push-notifications@molecule/api-secrets
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.