@playstack/notifications
Typed user-directed notifications across queued mail, push delivery, and a persisted in-app inbox.
Free. MIT licensed. Check preview availability before installing. See package access.
@playstack/notifications defines what a user-directed notification means, validates its input, applies channel preferences, and routes work to queued mail, push delivery, or a persisted in-app inbox. It does not own broadcasts, audiences, templates, provider clients, application routes, or SMS.
Install
npm install @playstack/notifications @playstack/delivery @playstack/eventsDefine and compose notifications
import { createNotifications, defineNotifications } from '@playstack/notifications'
const registry = defineNotifications({
'account.invitation.accepted': {
input: invitationAcceptedSchema,
category: 'transactional',
channels: ['mail', 'push', 'inApp'],
digestible: false,
render: {
mail: ({ input, locale }) => ({
payload: { template: 'invite', input, locale },
}),
push: ({ input }) => ({
payload: { title: 'Invitation accepted', body: input.summary },
}),
inApp: ({ input }) => ({
title: 'Invitation accepted',
body: input.summary,
href: '/members',
}),
},
},
})
const notifications = createNotifications({
registry,
preferences,
locales,
recipients,
queue,
delivery,
inbox,
events,
clock,
ids,
})enqueue() validates input and creates endpoint-free, per-channel jobs. process() revalidates the job, preference, locale, and current recipient endpoint before rendering or dispatching it. Push recipients resolve to opaque device-token IDs and fan out through @playstack/delivery/push; provider credentials are loaded only by the delivery worker.
Configuration reference
NotificationsOptions property | Required | Purpose |
|---|---|---|
registry | Yes | Typed definitions, Standard Schema input validators, channels, categories, and renderers. |
preferences | Yes | Resolves policy, quiet-hours, and marketing preferences per channel. |
locales | Yes | Resolves the recipient locale at enqueue and execution time. |
recipients | Yes | Resolves the current delivery subject and mail endpoint. |
queue | Yes | Enqueues endpoint-free notification jobs. |
delivery | Yes | Hands rendered mail payloads to @playstack/delivery. |
inbox | For in-app channels | Transactional inbox persistence; omit for mail/push-only registries. |
admissions | For keyed enqueue | Durable admission and queue-handoff replay persistence. |
events | Yes | Emits queued, delivered, failed, read, and archived lifecycle events. |
clock, ids | Yes | Application-owned time and identifiers. |
readRetentionMs | No | Read-row retention; defaults to 90 days. |
safeOrigins | No | Explicit HTTPS origins allowed in in-app notification links. |
Transactional notifications bypass only marketing-preference decisions. They still honor policy and quiet-hours decisions and always pass through delivery suppression.
Persistence and testing
Outbound-only composition
Mail/push-only registries can omit inbox. An in-app definition without an inbox
fails configuration, and inbox read/mutation/retention methods reject without an
adapter. Do not supply a dummy store to compose mail-only notifications.
Caller-keyed durable enqueue
import { PrismaNotificationAdmissionPersistence } from '@playstack/notifications/prisma'
const admissions = new PrismaNotificationAdmissionPersistence(prisma, {
namespace: 'product:production',
})
const notifications = createNotifications({ ...options, admissions })
await notifications.enqueue(name, input, recipient, context, {
idempotencyKey: 'invitation:opaque-id:accepted',
})This composition fragment requires the optional notifications.admissions.prisma
artifact and a root Prisma client. Persist the logical key with the producer's
domain event and reuse it on every retry. Unkeyed calls remain new notifications;
a key without admission persistence rejects before queue I/O. Conflicting input
or recipient under the same key rejects instead of changing the existing intent.
Admissions retain the original notification ID, validated portable JSON input, recipient, context and initial channel plan. Keep secrets, rendered content and raw addresses out of jobs/admissions; storage is not encrypted. Worker-time recipient and preference authority is rechecked.
A lost queue acknowledgment can repeat the same channel key. The queue/outbox must retain that key or a tombstone for the full replay lifetime, including after consumption; a short deduplication TTL or remove-on-complete is insufficient. Confirmed handoffs are recorded before queued-event emission, so that event can be retried without enqueueing again. Observational events remain at-least-once.
The host owns durable producer replay, downstream deduplication, retention and erasure. Admissions have no automatic expiry/deletion policy and do not join an arbitrary domain transaction. Queue acceptance is not provider delivery; pair with the durable delivery worker for provider attempts. Neither layer promises exactly-once delivery.
@playstack/notifications/prisma exports PrismaNotificationInboxPersistence, and the notifications.prisma artifact installs the inbox schema fragment. Applications own migration execution. @playstack/notifications/testing provides deterministic in-memory queue and inbox implementations.
In-app persistence and its delivered event share one transaction. Queue and observational event failures cannot make another notification channel fail.
API entry points and requirements
Reference snapshot: @playstack/notifications@0.1.0-beta.1. Import only the entry point your runtime needs. Paths below are relative to the installed package; use Go to Definition in your editor to inspect exact parameters, return types and overloads. Do not import the declaration-file paths directly.
| Public entry point | Declaration file |
|---|---|
@playstack/notifications | ./dist/index.d.ts |
@playstack/notifications/errors | ./dist/errors.d.ts |
@playstack/notifications/prisma | ./dist/prisma.d.ts |
@playstack/notifications/testing | ./dist/testing.d.ts |
@playstack/notifications/package.json | No TypeScript declaration (asset or metadata export). |
Node.js engine requirement: >=20. This is not a claim that every entry point works in browsers or Workers.
Peer dependencies
This package declares no peer dependencies. Its ordinary dependencies are resolved by the package manager.
For a complete first program, start with Getting started. For API lookup and partial-example conventions, see Reading the reference. Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.