On this page
  1. Install
  2. Compose and enqueue
  3. Durable attempts and replay
  4. Configuration reference
  5. Suppression semantics
  6. Deliver to registered devices
  7. API entry points and requirements
  8. Peer dependencies

@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:

sh
npm install --save-exact @playstack/delivery@0.1.0-beta.1

Check the peer requirements below before choosing a runtime or provider.

Compose and enqueue

ts
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 propertyRequiredPurpose
slotsYesNamed provider boundaries declaring channel, provider, sending domain, capabilities, and dispatch().
queueYesPersists the fully resolved delivery job.
persistenceYesTransactional subject, endpoint, channel, and topic suppressions.
normalizerYesApplies application-owned endpoint normalization.
hasherYesProduces keyed endpoint hashes for lookup without retaining raw addresses.
eventsYesEmits dispatch and suppression lifecycle events.
clock, idsYesApplication-owned time and identifiers.
slotResolverNoChooses a compatible slot; the default requires exactly one match.
onWarningNoObserves 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 pointDeclaration 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.jsonNo 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.

Go

Playstack Pro tag
OriginsPricingBlogNewsletterChangelogStatusRoadmap
ContributorsCommunityIn Use ShowcaseCase StudiesPartnersSponsors
FAQsSupportContact

© 2026 Playstack. All rights reserved.

With OSS
Terms of ServicePrivacy PolicyCookie PolicyImprint

By

Commune Software