Bas van den Aakster 3425ec92dc Docs: Add email notifications section with collection overrides examples
Add comprehensive email notification examples showing:
- How to use collectionOverrides to add custom hooks
- Integration with custom email services
- Integration with @xtr-dev/payload-mailing plugin
- Important notes about preserving existing hooks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-27 21:28:40 +01:00
2025-09-30 20:16:49 +02:00
2025-09-30 20:16:49 +02:00

@xtr-dev/payload-notifications

npm version

A PayloadCMS plugin that adds a configurable notifications collection for sending messages with titles, content, and attachable relationship items.

⚠️ Pre-release Warning: This package is currently in active development (v0.0.x). Breaking changes may occur before v1.0.0. Not recommended for production use.

Features

  • 📧 Notifications collection with title and message fields
  • 🔗 Configurable relationship attachments to any collection
  • 📱 Built-in read/unread status tracking
  • 🎯 Recipient targeting support
  • ⚙️ Flexible plugin configuration
  • 📅 Automatic timestamp tracking
  • 🔔 Optional web push notifications support (see WEBPUSH.md)

Installation

npm install @xtr-dev/payload-notifications

Basic Usage

Add the plugin to your Payload config:

import { buildConfig } from 'payload/config'
import { notificationsPlugin } from '@xtr-dev/payload-notifications'

export default buildConfig({
  plugins: [
    notificationsPlugin({
      // Basic configuration
    })
  ],
  // ... rest of your config
})

Configuration

Basic Configuration

notificationsPlugin({
  collections: {
    slug: 'notifications', // Default collection slug
  }
})

Advanced Configuration with Relationships

notificationsPlugin({
  collections: {
    slug: 'notifications',
    labels: {
      singular: 'Notification',
      plural: 'Notifications'
    }
  },
  relationships: [
    {
      name: 'order',
      relationTo: 'orders',
      label: 'Related Order'
    },
    {
      name: 'user',
      relationTo: 'users',
      label: 'Related User'
    },
    {
      name: 'product',
      relationTo: 'products',
      label: 'Related Product'
    }
  ],
  access: {
    // Custom access control functions
    read: ({ req }) => Boolean(req.user),
    create: ({ req }) => Boolean(req.user?.role === 'admin'),
    update: ({ req }) => Boolean(req.user?.role === 'admin'),
    delete: ({ req }) => Boolean(req.user?.role === 'admin'),
  },
  fields: [
    // Add custom fields to the notifications collection
  ]
})

For web push notifications setup, see WEBPUSH.md

Collection Schema

The plugin creates a notifications collection with the following fields:

  • title (required text): The notification title
  • message (required richText): The notification content
  • recipient (optional relationship): User who should receive the notification (optional if using custom recipient fields)
  • isRead (checkbox): Read status tracking
  • readAt (date): When the notification was read
  • attachments (group): Configurable relationship fields
  • createdAt/updatedAt: Automatic timestamps

API Usage

Creating Notifications

const notification = await payload.create({
  collection: 'notifications',
  data: {
    title: 'Order Shipped',
    message: [
      {
        children: [
          { text: 'Your order has been shipped and is on its way!' }
        ]
      }
    ],
    recipient: userId,
    attachments: {
      order: orderId,
      product: productId
    }
  }
})

Querying Notifications

// Get unread notifications for a user
const unreadNotifications = await payload.find({
  collection: 'notifications',
  where: {
    and: [
      { recipient: { equals: userId } },
      { isRead: { equals: false } }
    ]
  },
  sort: '-createdAt'
})

// Mark notification as read
await payload.update({
  collection: 'notifications',
  id: notificationId,
  data: {
    isRead: true,
    readAt: new Date()
  }
})

Plugin Options

Option Type Default Description
collections.slug string 'notifications' Collection slug
collections.labels object { singular: 'Notification', plural: 'Notifications' } Collection labels
relationships array [] Configurable relationship fields
access object Default access Custom access control functions
fields array [] Additional custom fields

Relationship Configuration

Each relationship in the relationships array supports:

{
  name: string;        // Field name in attachments group
  relationTo: string;  // Target collection slug
  label?: string;      // Admin UI label
  required?: boolean;  // Whether field is required
  hasMany?: boolean;   // Allow multiple selections
}

Examples

E-commerce Notifications

notificationsPlugin({
  relationships: [
    { name: 'order', relationTo: 'orders', label: 'Order' },
    { name: 'product', relationTo: 'products', label: 'Product', hasMany: true },
    { name: 'customer', relationTo: 'customers', label: 'Customer' }
  ]
})

Content Management Notifications

notificationsPlugin({
  relationships: [
    { name: 'post', relationTo: 'posts', label: 'Blog Post' },
    { name: 'page', relationTo: 'pages', label: 'Page' },
    { name: 'media', relationTo: 'media', label: 'Media', hasMany: true }
  ]
})

Email Notifications

You can add email functionality to notifications using the collectionOverrides option. This allows you to add custom hooks to the notifications collection without modifying the plugin code.

Using Collection Overrides

The key is to preserve existing hooks (like web push) while adding your own:

import { notificationsPlugin } from '@xtr-dev/payload-notifications'

notificationsPlugin({
  channels: [{ id: 'default', name: 'Default' }],
  collectionOverrides: {
    notifications: (config) => ({
      ...config,
      hooks: {
        ...config.hooks, // Preserve existing hooks (web push, etc.)
        afterChange: [
          ...(config.hooks?.afterChange || []), // Preserve existing afterChange hooks
          // Add your custom email hook
          async ({ doc, operation, req }) => {
            if (operation === 'create') {
              // Your email logic here
            }
          }
        ]
      }
    })
  }
})

Example: Custom Email Service

import { notificationsPlugin } from '@xtr-dev/payload-notifications'
import { sendEmail } from './your-email-service'
import { renderNotificationEmail } from './email-templates'

notificationsPlugin({
  channels: [{ id: 'default', name: 'Default' }],
  collectionOverrides: {
    notifications: (config) => ({
      ...config,
      hooks: {
        ...config.hooks,
        afterChange: [
          ...(config.hooks?.afterChange || []),
          async ({ doc, operation, req }) => {
            // Send email when notification is created
            if (operation === 'create') {
              try {
                // Get recipient user details
                let recipientId = doc.recipient
                if (typeof recipientId === 'object' && recipientId?.id) {
                  recipientId = recipientId.id
                }

                if (!recipientId) {
                  console.log('No recipient for email notification')
                  return
                }

                const recipient = await req.payload.findByID({
                  collection: 'users',
                  id: recipientId
                })

                if (!recipient?.email) {
                  console.log('Recipient has no email address')
                  return
                }

                // Send email
                await sendEmail({
                  to: recipient.email,
                  subject: doc.title,
                  html: renderNotificationEmail(doc)
                })

                console.log(`Email sent to ${recipient.email}`)
              } catch (error) {
                console.error('Failed to send notification email:', error)
                // Don't throw - we don't want to prevent notification creation
              }
            }
          }
        ]
      }
    })
  }
})

Example: Using @xtr-dev/payload-mailing

import { notificationsPlugin } from '@xtr-dev/payload-notifications'
import { mailingPlugin } from '@xtr-dev/payload-mailing'

// Configure the mailing plugin
const mailing = mailingPlugin({
  // ... your mailing config
})

// Add email notifications using collection overrides
const notifications = notificationsPlugin({
  channels: [{ id: 'default', name: 'Default' }],
  collectionOverrides: {
    notifications: (config) => ({
      ...config,
      hooks: {
        ...config.hooks,
        afterChange: [
          ...(config.hooks?.afterChange || []),
          async ({ doc, operation, req }) => {
            if (operation === 'create') {
              try {
                // Get recipient details
                let recipientId = doc.recipient
                if (typeof recipientId === 'object' && recipientId?.id) {
                  recipientId = recipientId.id
                }

                if (!recipientId) return

                const recipient = await req.payload.findByID({
                  collection: 'users',
                  id: recipientId
                })

                if (!recipient?.email) return

                // Extract plain text from rich text message
                const extractText = (richText: any[]): string => {
                  return richText
                    .map(block =>
                      block.children
                        ?.map((child: any) => child.text)
                        .join('')
                    )
                    .join('\n')
                }

                const messageText = extractText(doc.message)

                // Send email using payload-mailing
                await req.payload.create({
                  collection: 'emails',
                  data: {
                    to: recipient.email,
                    subject: doc.title,
                    text: messageText,
                    html: `
                      <h2>${doc.title}</h2>
                      <div>${messageText}</div>
                      <p><a href="${process.env.PAYLOAD_PUBLIC_URL}/admin/collections/notifications/${doc.id}">View notification</a></p>
                    `,
                  }
                })

                console.log(`Email queued for ${recipient.email}`)
              } catch (error) {
                console.error('Failed to send notification email:', error)
              }
            }
          }
        ]
      }
    })
  }
})

export default buildConfig({
  plugins: [
    mailing,
    notifications,
  ],
  // ... rest of config
})

Important Notes:

  • Always spread existing hooks (...config.hooks) to preserve plugin functionality
  • Use the spread operator for hook arrays (...(config.hooks?.afterChange || []))
  • Don't throw errors in hooks if you want to allow notification creation to succeed even if email fails
  • Email sending happens asynchronously after the notification is created

Web Push Notifications

The plugin includes optional web push notifications support for PWA and mobile browser users. For complete setup instructions, configuration options, and usage examples, see WEBPUSH.md.

TypeScript Support

The plugin includes full TypeScript support. Types are automatically generated based on your configuration.

License

MIT

Description
No description provided
Readme 224 KiB
Languages
TypeScript 90.9%
JavaScript 9.1%