{"_id":"@elghaied/payload-plugin-notifications","name":"@elghaied/payload-plugin-notifications","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@elghaied/payload-plugin-notifications","version":"1.0.0","description":"In-dashboard notifications for the Payload admin, with a live SSE bell.","author":{"name":"elghaied"},"license":"MIT","homepage":"https://github.com/elghaied/payload-plugin-notifications#readme","bugs":{"url":"https://github.com/elghaied/payload-plugin-notifications/issues"},"repository":{"type":"git","url":"git+https://github.com/elghaied/payload-plugin-notifications.git"},"keywords":["payload","payloadcms","payload-plugin","plugin","notifications","in-app-notifications","notification-bell","sse","server-sent-events","realtime","admin","dashboard","i18n","multi-tenant","typescript"],"type":"module","exports":{".":{"import":"./dist/index.js","types":"./dist/index.d.ts","default":"./dist/index.js"},"./client":{"import":"./dist/exports/client.js","types":"./dist/exports/client.d.ts","default":"./dist/exports/client.js"},"./rsc":{"import":"./dist/exports/rsc.js","types":"./dist/exports/rsc.d.ts","default":"./dist/exports/rsc.js"}},"main":"./dist/index.js","types":"./dist/index.d.ts","devDependencies":{"@eslint/eslintrc":"^3.2.0","@payloadcms/db-mongodb":"3.84.1","@payloadcms/translations":"3.84.1","@payloadcms/db-postgres":"3.84.1","@payloadcms/db-sqlite":"3.84.1","@payloadcms/eslint-config":"3.28.0","@payloadcms/next":"3.84.1","@payloadcms/richtext-lexical":"3.84.1","@payloadcms/ui":"3.84.1","@playwright/test":"1.58.2","@swc-node/register":"1.10.9","@swc/cli":"0.6.0","@types/node":"22.19.9","@types/react":"19.2.14","@types/react-dom":"19.2.3","copyfiles":"2.4.1","cross-env":"^7.0.3","eslint":"^9.23.0","eslint-config-next":"16.2.6","graphql":"^16.8.1","mongodb-memory-server":"10.1.4","next":"16.2.6","open":"^10.1.0","payload":"3.84.1","prettier":"^3.4.2","qs-esm":"8.0.1","react":"19.2.6","react-dom":"19.2.6","rimraf":"3.0.2","sharp":"0.34.2","sort-package-json":"^2.10.0","typescript":"5.7.3","vite-tsconfig-paths":"6.0.5","vitest":"4.0.18"},"peerDependencies":{"@payloadcms/translations":"^3.84.1","payload":"^3.84.1"},"engines":{"node":"^18.20.2 || >=20.9.0","pnpm":"^9 || ^10"},"registry":"https://registry.npmjs.org/","dependencies":{},"scripts":{"build":"pnpm copyfiles && pnpm build:types && pnpm build:swc","build:swc":"swc ./src -d ./dist --config-file .swcrc --strip-leading-paths","build:types":"tsc --outDir dist --rootDir ./src","clean":"rimraf {dist,*.tsbuildinfo}","copyfiles":"copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/","dev":"cross-env PAYLOAD_MEMORY_DB=true next dev dev --turbo","dev:generate-importmap":"pnpm dev:payload generate:importmap","dev:generate-types":"pnpm dev:payload generate:types","dev:payload":"node ./dev/payload-cli.mjs","generate:importmap":"pnpm dev:generate-importmap","generate:types":"pnpm dev:generate-types","lint":"eslint","lint:fix":"eslint ./src --fix","test":"pnpm test:int && pnpm test:e2e","test:e2e":"playwright test","test:int":"vitest","start":"cross-env PAYLOAD_MEMORY_DB=true next build dev && cross-env PAYLOAD_MEMORY_DB=true next start dev"},"_id":"@elghaied/payload-plugin-notifications@1.0.0","_integrity":"sha512-h83URTBJ8ygbzHg+BpqggS5UNvoFV8cB4EK+RZDDXOUYzTSDxIXcNrtc56V72Etcbz7fjduL5HWkd/zV9MBUag==","_resolved":"/tmp/0450701234132a04945f7a716520b2b7/elghaied-payload-plugin-notifications-1.0.0.tgz","_from":"file:elghaied-payload-plugin-notifications-1.0.0.tgz","_nodeVersion":"20.20.2","_npmVersion":"11.16.0","dist":{"integrity":"sha512-h83URTBJ8ygbzHg+BpqggS5UNvoFV8cB4EK+RZDDXOUYzTSDxIXcNrtc56V72Etcbz7fjduL5HWkd/zV9MBUag==","shasum":"065c80a5de0e6606d958949a77cb7d4b21ec9d4e","tarball":"https://registry.npmjs.org/@elghaied/payload-plugin-notifications/-/payload-plugin-notifications-1.0.0.tgz","fileCount":93,"unpackedSize":121771,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@elghaied%2fpayload-plugin-notifications@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDgcnaeOi34JZN1j1H/gFPB4xts/Zu6UqOFRtIMzWB7NAIgRWzbyzfPor+fQfq75h8zOAcDEMKJDWfAYdv6ZNT1WgI="}]},"_npmUser":{"name":"elghaied","email":"elghaied.eslam@gmail.com"},"directories":{},"maintainers":[{"name":"elghaied","email":"elghaied.eslam@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/payload-plugin-notifications_1.0.0_1780825981917_0.46489733523651156"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-07T09:53:01.717Z","1.0.0":"2026-06-07T09:53:02.082Z","modified":"2026-06-07T09:53:02.491Z"},"maintainers":[{"name":"elghaied","email":"elghaied.eslam@gmail.com"}],"description":"In-dashboard notifications for the Payload admin, with a live SSE bell.","homepage":"https://github.com/elghaied/payload-plugin-notifications#readme","keywords":["payload","payloadcms","payload-plugin","plugin","notifications","in-app-notifications","notification-bell","sse","server-sent-events","realtime","admin","dashboard","i18n","multi-tenant","typescript"],"repository":{"type":"git","url":"git+https://github.com/elghaied/payload-plugin-notifications.git"},"author":{"name":"elghaied"},"bugs":{"url":"https://github.com/elghaied/payload-plugin-notifications/issues"},"license":"MIT","readme":"# @elghaied/payload-plugin-notifications\n\nIn-dashboard notifications for the [Payload](https://payloadcms.com) admin, with a **live bell**\nthat updates without a page refresh.\n\n- 🔔 A `NotificationBell` in the admin header — unread count + dropdown feed with compact relative\n  timestamps (\"now\", \"40m\", \"2d\"); unread rows highlighted, read rows dimmed; click a row to mark\n  it read and go where it points.\n- 📋 A **custom notifications list view** — Payload's default table is replaced with a clean,\n  paginated feed (same row, whole-row click marks read + navigates). Reached via \"See all\".\n- ⚡ **Live** via Server-Sent Events (best-effort), with graceful degradation when SSE can't connect.\n- 🗄️ Every notification is a real DB row (the source of truth) — **adapter-agnostic** (Mongo or Postgres), opaque ids.\n- 🔒 Notification `link`s are scheme-validated before navigation (no `javascript:`/`data:` XSS).\n- 🧩 Additive and **default-off**; **optional** multi-tenant scoping.\n\n## Screenshots\n\n| Bell dropdown | Custom list view |\n|---|---|\n| ![Notification bell dropdown open over the admin, showing recent notifications with relative timestamps](./assets/notifications_bell.png) | ![Custom notifications list view replacing Payload's default table](./assets/notifications_list.png) |\n\nUnread rows carry an accent bar and bolded text; read rows are dimmed. Times are compact and relative (`now`, `8m`, `3h`, `2d`, `1w`), with the full timestamp on hover.\n\n## How it works\n\nTwo decoupled layers:\n\n1. **Source of truth** — a `notifications` collection row. Created via `pushNotification()`. A user\n   sees their notifications whether or not a tab was open when they were created.\n2. **Live channel** — an `afterChange` hook on the collection emits each new row over SSE to that\n   recipient's open admin tabs (in-memory connection registry). This is the *only* live-push path,\n   and it's a side effect of the DB write — never a separate path.\n\n> Designed for a long-running Node process (self-hosted), where SSE connections hold. On serverless\n> or behind a buffering proxy the bell still shows accurate counts on mount and on dropdown-open —\n> it just won't pop instantly.\n\n## Install\n\n```bash\npnpm add @elghaied/payload-plugin-notifications\n```\n\n## Usage\n\n```ts\n// payload.config.ts\nimport { payloadPluginNotifications } from '@elghaied/payload-plugin-notifications'\n\nexport default buildConfig({\n  plugins: [\n    payloadPluginNotifications(),\n  ],\n})\n```\n\nThat adds the `notifications` collection (hidden from the nav) and the live bell in the admin header.\n\n### Sending a notification\n\n`pushNotification` is a thin wrapper over `payload.create` (with `overrideAccess`), callable from any\nhook, endpoint, or job:\n\n```ts\nimport { pushNotification } from '@elghaied/payload-plugin-notifications'\n\nawait pushNotification(payload, {\n  recipient: user.id,\n  message: 'Invoice #1042 was paid',\n  link: '/admin/collections/invoices/1042',\n  type: 'success', // 'info' | 'warning' | 'success'\n})\n\n// Inside a hook, pass `req` so the create joins the same transaction:\nawait pushNotification(req.payload, { recipient, message: 'Updated', req })\n```\n\nREST `create` is denied — notifications are system-generated. Each user can only read, mark-read,\nand dismiss their **own** notifications.\n\n## Configuration\n\n```ts\npayloadPluginNotifications({\n  disabled,          // boolean   — default false. Installed but inert; DB schema stays stable.\n  notificationsSlug, // string    — default 'notifications'\n  usersSlug,         // string    — default 'users'. The auth collection notifications target.\n  tenants: {         // object    — OMIT for single-tenant (recipient-only scoping).\n    tenantsSlug,     //   string  — default 'tenants'\n    tenantFieldName, //   string  — default 'tenant'\n  },\n  hideFromNav,       // boolean   — default true. The bell is the UI surface.\n})\n```\n\n### Multi-tenancy\n\nWhen the `tenants` block is set, the plugin adds a required `tenant` relationship field and ANDs a\ntenant filter into read access. It interoperates with `@payloadcms/plugin-multi-tenant` but doesn't\ndepend on it.\n\n> The default filter assumes **one tenant per user** (`user[tenantFieldName]`). The official\n> multi-tenant plugin models users with a `tenants` *array* — for that model you'll want a custom\n> tenant filter (a `tenantFilter` config hook is the planned extension point).\n\n## Notifications collection\n\n| field | type | notes |\n|---|---|---|\n| `recipient` | relationship → users | required |\n| `tenant` | relationship → tenants | only when multi-tenant is enabled (required) |\n| `message` | text | required |\n| `link` | text | e.g. `/admin/collections/invoices/123` |\n| `type` | select | `info` \\| `warning` \\| `success` |\n| `read` | checkbox | default `false` |\n\n## Extension points (not built in v1)\n\n- **Background jobs / digests** — `pushNotification()` is the single seam where `payload.jobs.queue()`\n  would swap in for async or scheduled/digest notifications.\n- **Multi-replica live push** — the `NotificationRegistry` interface (in-memory today) accepts a\n  distributed implementation (Postgres `LISTEN/NOTIFY` or Mongo change streams). No Redis.\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-063b2f88de23cfe93c291026d80eec19"}