---
title: "@playstack/delivery"
description: "Queued outbound dispatch with shared suppression policy and independently configured provider slots."
tags: ["package","communication","delivery","suppression","queues","free"]
---

{/* package-access:start */}

> **Free.** MIT licensed. Check preview availability before installing. See [package access](/docs/packages#access-policy).

{/* package-access:end */}

`@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.

{/* package-install:start */}

## Install

After confirming [preview access](/docs/packages#access-policy), 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.

{/* package-install:end */}

## 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](/docs/packages/foundation/prisma-runtime#node-prismapg-dispatch-fence).

## 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.

{/* package-reference:start */}

## 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](/docs/getting-started). For API lookup and partial-example conventions, see [Reading the reference](/docs/packages#reading-the-reference). Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.

{/* package-reference:end */}
