---
title: "@playstack/notifications"
description: "Typed user-directed notifications across queued mail, push delivery, and a persisted in-app inbox."
tags: ["package","communication","notifications","inbox","mail","free"]
---

{/* package-access:start */}

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

{/* package-access:end */}

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

```sh
npm install @playstack/notifications @playstack/delivery @playstack/events
```

## Define and compose notifications

```ts
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

```ts
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](/docs/packages/communication/delivery#durable-attempts-and-replay)
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.

{/* package-reference:start */}

## 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](/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 */}
