@playstack/delivery
Queued outbound dispatch with shared suppression policy and independently configured provider slots.
Free. MIT licensed. Check preview availability before installing. See package access.
@playstack/delivery normalizes outbound dispatch across mail, broadcast, push, in-app, and SMS slots while enforcing one suppression model. It does not own audiences, consent, message composition, provider DNS, or durable delivery history.
Install
After confirming preview access, install the package at your application's shared Playstack version:
npm install --save-exact @playstack/delivery@0.1.0-beta.1Check the peer requirements below before choosing a runtime or provider.
Compose and enqueue
import { createDelivery } from '@playstack/delivery'
const delivery = createDelivery({
slots: [transactionalMail, broadcastMail, push],
queue,
persistence: suppressions,
normalizer: endpoints,
hasher: keyedEndpointHasher,
events,
clock,
ids,
})
await delivery.enqueue({
idempotencyKey: 'password-reset:user-123:token-456',
subject: { type: 'user', id: 'user-123' },
endpoint: { channel: 'mail', value: 'person@example.com' },
category: 'transactional',
payload: { template: 'password-reset', tokenId: 'token-456' },
})The service checks suppression before enqueue and again before provider dispatch.
That check cannot recall an in-flight submission or atomically lock a remote
provider together with consent. DeliveryService.process() is immediate dispatch:
it does not persist attempts or durably deduplicate repeated calls.
Durable attempts and replay
Use the optional createDeliveryWorker with PrismaDeliveryAttemptPersistence
from @playstack/delivery/attempts/prisma when a queue must survive crashes and
uncertain acknowledgments. Select and migrate delivery.attempts.prisma; provide
the actual root Prisma client, not an enclosing transaction/savepoint.
Register immutable intent, its protected payload reference and the host outbox
record in one physical transaction before publishing work. The worker's prepare
callback uses its supplied transaction handle to check current authorization and
suppression, resolve protected content and recompute the request hash. Do not
trust a queued hash or recipient as authorization.
Only a newly committed submission transition grants provider I/O. Replayed begins, lost commit acknowledgments, active submissions and quarantined expiries do not grant another send. Provider exceptions and malformed receipts become unknown outcomes by default, not permission to retry or fail over to another transport. Keep provider SDK retries under the same policy.
The recorded callback shares the outcome transaction. A
DeliveryReceiptRecordingError preserves accepted receipt evidence when local
recording fails: repair that recording without resending. Acceptance is not inbox
delivery. Use explicitly authorized reconciliation for safe retries.
The worker returns state, not an unconditional queue acknowledgment. Acknowledge only a durable terminal disposition or durable recovery handoff; pending, leased or submitting work still needs recovery. The host owns queue retention, outbox publication, recovery scheduling and shutdown. Neither composition promises exactly-once external delivery. Qualify the actual database/driver transaction boundary; see Prisma runtime.
Configuration reference
DeliveryOptions property | Required | Purpose |
|---|---|---|
slots | Yes | Named provider boundaries declaring channel, provider, sending domain, capabilities, and dispatch(). |
queue | Yes | Persists the fully resolved delivery job. |
persistence | Yes | Transactional subject, endpoint, channel, and topic suppressions. |
normalizer | Yes | Applies application-owned endpoint normalization. |
hasher | Yes | Produces keyed endpoint hashes for lookup without retaining raw addresses. |
events | Yes | Emits dispatch and suppression lifecycle events. |
clock, ids | Yes | Application-owned time and identifiers. |
slotResolver | No | Chooses a compatible slot; the default requires exactly one match. |
onWarning | No | Observes potentially unsafe configuration such as shared mail and broadcast domains. |
Each DeliverySlot exposes provider capabilities and remains the native-client escape hatch. Applications can route by tenant or region through a custom DeliverySlotResolver.
Suppression semantics
Marketing sends honor subject, endpoint, channel, and topic suppressions. Transactional sends bypass marketing-only channel and topic opt-outs, but never subject-wide or invalid-endpoint suppression.
addSuppressionInTransaction() and removeSuppressionInTransaction() let another
feature change its state and suppression atomically. Both adapters must join the
same physical transaction; matching transaction-kind strings alone are not proof.
For remote-authoritative audiences without outbound delivery, use
createDeliverySuppressionService({ persistence, normalizer, hasher, events, clock, ids }).
It implements DeliverySuppressionWriter with the same native persistence and
events, without delivery slots, provider clients or a queue.
Deliver to registered devices
@playstack/delivery/push resolves encrypted targets owned by @playstack/devices immediately before provider I/O, keeping raw push credentials out of queued jobs.
Named adapters under /apns, /expo, /fcm, and /webpush accept application-created
clients. Outcomes include accepted, transient-failure, permanent-failure and
outcome-unknown. Unknown outcomes are non-retryable uncertainty, not evidence of an
invalid token. Only provider-confirmed permanent failures invalidate the target.
An application-owned PushTargetSource can replace registered-device persistence;
it must preserve live authorization and permanent-failure feedback.
API entry points and requirements
Reference snapshot: @playstack/delivery@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/delivery/attempts/prisma | ./dist/attempts-prisma.d.ts |
@playstack/delivery/attempts | ./dist/attempts.d.ts |
@playstack/delivery/prisma | ./dist/prisma.d.ts |
@playstack/delivery | ./dist/index.d.ts |
@playstack/delivery/errors | ./dist/errors.d.ts |
@playstack/delivery/push | ./dist/push.d.ts |
@playstack/delivery/expo | ./dist/expo.d.ts |
@playstack/delivery/apns | ./dist/apns.d.ts |
@playstack/delivery/fcm | ./dist/fcm.d.ts |
@playstack/delivery/webpush | ./dist/webpush.d.ts |
@playstack/delivery/testing | ./dist/testing.d.ts |
@playstack/delivery/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.