npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

payload-plugin-newsletter

v0.25.9

Published

Complete newsletter management plugin for Payload CMS with subscriber management, magic link authentication, and email service integration

Readme

Payload Newsletter Plugin

npm version License: MIT

A complete newsletter management plugin for Payload CMS that provides subscriber management, magic link authentication, and email service integration out of the box.

Important: Version 0.8.7+ includes critical fixes for Payload v3 compatibility. If you're using Payload v3, please ensure you're on at least version 0.8.7 of this plugin.

Features

  • 📧 Complete Subscriber Management - Ready-to-use subscriber collection with all essential fields
  • 🔐 Magic Link Authentication - Passwordless authentication for subscribers (separate from Payload auth)
  • 📨 Email Service Integration - Built-in support for Resend and Broadcast
  • 📅 Newsletter Scheduling - Schedule newsletters from your articles collection
  • ⚛️ React Components - Pre-built signup forms and preference management UI
  • 🌍 Internationalization - Multi-language support built-in
  • 📊 Analytics Ready - UTM tracking and signup metadata collection
  • ⚙️ Admin UI Configuration - Manage email settings through Payload admin panel
  • 🔄 Real-time Webhook Sync - Receive subscriber and broadcast events from email services via webhooks
  • 👁️ Email Preview - Real-time preview with desktop/mobile views (v0.9.0+)
  • Email Validation - Built-in validation for email client compatibility (v0.9.0+)
  • 📝 Email-Safe Editor - Rich text editor limited to email-compatible features (v0.9.0+)
  • 📬 Broadcast Management - Create and send email campaigns with provider sync (v0.10.0+)
  • 🎨 React Email Templates - Customizable email templates with React Email (v0.12.0+)

Prerequisites

  • Payload CMS v3.0.0 or higher
  • A Media collection configured in your Payload project (required for image support in broadcasts)

Quick Start

1. Install the plugin

bun add payload-plugin-newsletter
# or
npm install payload-plugin-newsletter
# or
yarn add payload-plugin-newsletter
# or
pnpm add payload-plugin-newsletter

2. Add to your Payload config

import { buildConfig } from 'payload/config'
import { newsletterPlugin } from 'payload-plugin-newsletter'

export default buildConfig({
  plugins: [
    newsletterPlugin({
      // Choose your email provider
      providers: {
        default: 'resend', // or 'broadcast'
        resend: {
          apiKey: process.env.RESEND_API_KEY,
          fromAddress: '[email protected]',
          fromName: 'Your Newsletter',
          audienceIds: {
            en: {
              production: 'your_audience_id',
              development: 'your_dev_audience_id',
            },
          },
        },
      },
    }),
  ],
  // ... rest of your config
})

3. That's it! 🎉

The plugin automatically adds:

  • A subscribers collection to manage your subscribers
  • A newsletter-settings collection for email configurations (supports multiple environments)
  • API endpoints for subscription and authentication
  • Newsletter scheduling fields to your articles (optional)

Basic Usage

Frontend Integration

Simple Newsletter Signup Form

import { NewsletterForm } from 'payload-plugin-newsletter/components'

export function MyHomepage() {
  return (
    <NewsletterForm 
      onSuccess={() => console.log('Subscribed!')}
      onError={(error) => console.error(error)}
    />
  )
}

Custom Signup Form

async function handleSubscribe(email: string) {
  const response = await fetch('/api/newsletter/subscribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email }),
  })
  
  if (!response.ok) {
    throw new Error('Subscription failed')
  }
  
  return response.json()
}

Managing Subscribers

Subscribers can be managed through the Payload admin panel at /admin/collections/subscribers.

Email Settings

After setup, configure email settings at /admin/collections/newsletter-settings in your admin panel. You can:

  • Create multiple configurations (e.g., for different environments or purposes)
  • Set one configuration as active at a time
  • Switch between email providers
  • Update API keys and settings
  • Customize email templates
  • Set subscription preferences

Note: Only one configuration can be active at a time. The plugin will use the active configuration for sending emails.

Initial Setup

After installing the plugin, you'll need to:

  1. Create an email configuration:

    • Go to /admin/collections/newsletter-settings
    • Click "Create New"
    • Give it a name (e.g., "Production" or "Development")
    • Configure your email provider settings
    • Set it as "Active"
    • Save
  2. Start collecting subscribers:

    • Subscribers will appear in /admin/collections/subscribers
    • Use the provided React components or API endpoints

Email Preview Features (v0.9.0+)

The plugin includes comprehensive email preview functionality to ensure your newsletters look great across all email clients.

Email-Safe Rich Text Editor

The plugin provides a pre-configured Lexical editor with only email-compatible features:

import { createEmailContentField } from 'payload-plugin-newsletter/fields'

const BroadcastsCollection = {
  fields: [
    createEmailContentField({
      name: 'content',
      required: true,
    })
  ]
}

Features included:

  • Basic text formatting (bold, italic, underline, strikethrough)
  • Simple links
  • Ordered and unordered lists
  • Headings (H1, H2, H3)
  • Text alignment
  • Blockquotes

Real-Time Email Preview

The plugin includes a preview component that shows how your email will look:

{
  name: 'preview',
  type: 'ui',
  admin: {
    components: {
      Field: 'payload-plugin-newsletter/components/EmailPreviewField'
    }
  }
}

Preview features:

  • Desktop & Mobile Views - Switch between viewport sizes
  • Live Updates - See changes as you type
  • Validation Warnings - Catch compatibility issues before sending
  • Test Email - Send a test to your inbox

Email HTML Validation

Built-in validation checks for:

  • HTML size limits (Gmail's 102KB limit)
  • Unsupported CSS properties
  • Missing alt text on images
  • External resources that won't load
  • JavaScript that will be stripped

Broadcast Management (v0.10.0+)

Create and send email campaigns directly from Payload:

Enable Broadcasts

newsletterPlugin({
  features: {
    newsletterManagement: {
      enabled: true,
    }
  },
  providers: {
    default: 'broadcast',
    broadcast: {
      apiUrl: process.env.BROADCAST_API_URL,
      token: process.env.BROADCAST_TOKEN,
      fromAddress: '[email protected]',
      fromName: 'Your Newsletter',
    }
  }
})

This adds a broadcasts collection with:

  • Rich text editor with email-safe formatting
  • Image uploads with Media collection integration
  • Custom email blocks (buttons, dividers)
  • Inline email preview with React Email
  • Automatic sync with your email provider
  • Draft/publish system with scheduled publishing support

Send = Publish Workflow

The plugin integrates seamlessly with Payload's draft/publish system:

  • Draft: Create and edit broadcasts without sending
  • Publish: Publishing a broadcast automatically sends it via your configured email provider
  • Schedule: Use Payload's scheduled publishing to send broadcasts at a future time

How it works:

  1. Create a broadcast and save as draft
  2. When ready, click "Publish" to send immediately
  3. Or use "Schedule" to publish (and send) at a specific date/time

Important: Scheduled publishing requires configuring Payload's Jobs Queue. For Vercel deployments, add this to your vercel.json:

{
  "crons": [
    {
      "path": "/api/payload-jobs/run",
      "schedule": "*/5 * * * *"
    }
  ]
}

And secure the endpoint in your payload.config.ts:

export default buildConfig({
  // ... other config
  jobs: {
    access: {
      run: ({ req }) => {
        if (req.user) return true
        const authHeader = req.headers.get('authorization')
        return authHeader === `Bearer ${process.env.CRON_SECRET}`
      },
    },
  },
})

Custom Email Templates (v0.12.0+)

Customize your email design with React Email templates:

// email-templates/broadcast-template.tsx
import { Html, Body, Container, Text, Link } from '@react-email/components'

export default function BroadcastTemplate({ subject, preheader, content }) {
  return (
    <Html>
      <Body style={{ backgroundColor: '#ffffff', fontFamily: 'Arial, sans-serif' }}>
        <Container style={{ maxWidth: '600px', margin: '0 auto' }}>
          <Text style={{ fontSize: '16px', lineHeight: '1.6' }}>
            <div dangerouslySetInnerHTML={{ __html: content }} />
          </Text>
          <hr style={{ margin: '40px 0', border: '1px solid #e5e7eb' }} />
          <Text style={{ fontSize: '14px', color: '#6b7280', textAlign: 'center' }}>
            <Link href="{{unsubscribe_url}}" style={{ color: '#6b7280' }}>
              Unsubscribe
            </Link>
          </Text>
        </Container>
      </Body>
    </Html>
  )
}

The plugin automatically detects templates at email-templates/broadcast-template.tsx.

Utilities

Convert Lexical content to email-safe HTML:

import { convertToEmailSafeHtml } from 'payload-plugin-newsletter/utils'

const html = await convertToEmailSafeHtml(editorState)

Validate any HTML for email compatibility:

import { validateEmailHtml } from 'payload-plugin-newsletter/utils'

const result = validateEmailHtml(html)
if (!result.valid) {
  console.error('Email issues:', result.errors)
}

Configuration Options

Minimal Configuration

newsletterPlugin({
  providers: {
    default: 'resend',
    resend: {
      apiKey: process.env.RESEND_API_KEY,
      fromAddress: '[email protected]',
      fromName: 'Your Newsletter',
    },
  },
})

Full Configuration

newsletterPlugin({
  // Subscriber collection slug (default: 'subscribers')
  subscribersSlug: 'newsletter-subscribers',
  
  // Email providers
  providers: {
    default: 'resend',
    resend: {
      apiKey: process.env.RESEND_API_KEY,
      fromAddress: '[email protected]',
      fromName: 'Your Newsletter',
      audienceIds: {
        en: {
          production: 'aud_prod_123',
          development: 'aud_dev_123',
        },
        es: {
          production: 'aud_prod_456',
          development: 'aud_dev_456',
        },
      },
    },
  },
  
  // Magic link authentication
  auth: {
    enabled: true,
    tokenExpiration: '7d', // How long magic links are valid
    magicLinkPath: '/newsletter/verify', // Where to redirect for verification
  },
  
  // Features
  features: {
    // Lead magnets (e.g., downloadable PDFs)
    leadMagnets: {
      enabled: true,
      collection: 'media', // Which collection stores your lead magnets
    },
    
    // Post-signup surveys
    surveys: {
      enabled: true,
      questions: [
        {
          id: 'interests',
          question: 'What topics interest you?',
          type: 'multiselect',
          options: ['Tech', 'Business', 'Design'],
        },
      ],
    },
    
    // Newsletter scheduling for articles
    newsletterScheduling: {
      enabled: true,
      articlesCollection: 'posts', // Your articles/posts collection
    },
    
    // Broadcast management (v0.10.0+)
    newsletterManagement: {
      enabled: true, // Enables broadcasts collection
    },
    
    // UTM tracking
    utmTracking: {
      enabled: true,
      fields: ['source', 'medium', 'campaign', 'content', 'term'],
    },
  },
  
  // Internationalization
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'es', 'fr'],
  },
  
  // Custom hooks
  hooks: {
    afterSubscribe: async ({ doc, req }) => {
      // Send to analytics, CRM, etc.
      console.log('New subscriber:', doc.email)
    },
  },
})

API Endpoints

The plugin adds these endpoints to your application:

POST /api/newsletter/subscribe

Subscribe a new email address

// Request
{
  "email": "[email protected]",
  "name": "John Doe", // optional
  "preferences": { // optional
    "newsletter": true,
    "announcements": false
  }
}

// Response
{
  "success": true,
  "subscriber": { /* subscriber object */ }
}

POST /api/newsletter/verify-magic-link

Verify a magic link token

// Request
{
  "token": "eyJhbGc..."
}

// Response
{
  "success": true,
  "subscriber": { /* subscriber object */ },
  "sessionToken": "eyJhbGc..."
}

GET/POST /api/newsletter/preferences

Get or update subscriber preferences (requires magic link auth)

POST /api/newsletter/unsubscribe

Unsubscribe an email address

POST /api/newsletter/signin

Request a magic link for existing subscribers

// Request
{
  "email": "[email protected]"
}

// Response
{
  "success": true,
  "message": "Check your email for the sign-in link"
}

GET /api/newsletter/me

Get current authenticated subscriber (requires authentication)

// Response
{
  "success": true,
  "subscriber": {
    "id": "123",
    "email": "[email protected]",
    "name": "John Doe",
    "status": "active",
    "preferences": { /* preferences */ }
  }
}

POST /api/newsletter/signout

Sign out the current subscriber

// Response
{
  "success": true,
  "message": "Signed out successfully"
}

Authentication

The plugin provides complete magic link authentication for subscribers:

Client-Side Authentication

Use the useNewsletterAuth hook in your React components:

import { useNewsletterAuth } from 'payload-plugin-newsletter/client'

function MyComponent() {
  const { 
    subscriber, 
    isAuthenticated, 
    isLoading, 
    signOut, 
    refreshAuth 
  } = useNewsletterAuth()
  
  if (isLoading) return <div>Loading...</div>
  
  if (!isAuthenticated) {
    return <div>Please sign in to manage your preferences</div>
  }
  
  return (
    <div>
      <p>Welcome {subscriber.email}!</p>
      <button onClick={signOut}>Sign Out</button>
    </div>
  )
}

Server-Side Authentication

For Next.js applications, use the session utilities:

import { requireAuth, getServerSideAuth } from 'payload-plugin-newsletter'

// Protect a page - redirects to /auth/signin if not authenticated
export const getServerSideProps = requireAuth()

// Or with custom logic
export const getServerSideProps = requireAuth(async (context) => {
  // Your custom logic here
  const data = await fetchData()
  return { props: { data } }
})

// Manual authentication check
export const getServerSideProps = async (context) => {
  const { subscriber, isAuthenticated } = await getServerSideAuth(context)
  
  if (!isAuthenticated) {
    // Handle unauthenticated state
  }
  
  return {
    props: { subscriber }
  }
}

Authentication Flow

  1. Subscribe: New users receive a magic link email to verify their email
  2. Sign In: Existing subscribers can request a new magic link via /api/newsletter/signin
  3. Verify: Clicking the magic link verifies the email and creates a session
  4. Session: Sessions are stored in httpOnly cookies (30-day expiry by default)
  5. Sign Out: Clears the session cookie

Configuration

newsletterPlugin({
  auth: {
    enabled: true, // Enable/disable authentication
    tokenExpiration: '7d', // Magic link validity
    magicLinkPath: '/newsletter/verify', // Verification redirect path
  },
  // Email templates can be customized
  emails: {
    magicLink: {
      subject: 'Sign in to {{siteName}}',
    },
    welcome: {
      enabled: true,
      subject: 'Welcome to {{siteName}}!',
    },
    signIn: {
      subject: 'Sign in to your account',
    },
  },
})

Newsletter Scheduling

If you enable newsletter scheduling, the plugin adds scheduling fields to your articles collection:

features: {
  newsletterScheduling: {
    enabled: true,
    articlesCollection: 'articles', // Your existing collection
  }
}

This adds a "Newsletter Scheduling" group to your articles with:

  • Schedule toggle
  • Send date/time picker
  • Audience segment selection
  • Send status tracking

Webhook Configuration (Broadcast)

The plugin supports real-time webhook integration with Broadcast for instant updates:

Automatic Updates

When configured, the plugin automatically receives and processes:

  • Subscriber Events: subscribed, unsubscribed
  • Broadcast Events: All status changes (scheduled, in_progress, sent, etc.)

Setup Instructions

  1. Save your Newsletter Settings in the Payload admin to generate a webhook URL
  2. Configure in Broadcast:
    • Go to your Broadcast dashboard → "Webhook Endpoints"
    • Click "Add Webhook Endpoint"
    • Paste the webhook URL from Payload
    • Select events:
      • Subscriber Events: subscribed, unsubscribed
      • Broadcast Events: All
    • Create the webhook and copy the webhook secret
  3. Add the webhook secret to your Newsletter Settings in Payload
  4. Save and verify the webhook connection

Security

Webhooks are secured with:

  • HMAC-SHA256 signature verification
  • Timestamp validation (5-minute window)
  • Secret key stored in Newsletter Settings

Data Flow

  • Subscriber events update the subscriber's status and metadata
  • Broadcast events update the broadcast's status and delivery metrics
  • All updates happen in real-time without polling

Note: Email engagement events (opens, clicks) remain in Broadcast for analytics.

Email Providers

Resend

Resend is a modern email API for developers.

providers: {
  default: 'resend',
  resend: {
    apiKey: process.env.RESEND_API_KEY,
    fromAddress: '[email protected]',
    fromName: 'Your Newsletter',
    audienceIds: {
      en: {
        production: 'your_audience_id',
      },
    },
  },
}

Broadcast

Broadcast is a self-hosted email automation platform.

providers: {
  default: 'broadcast',
  broadcast: {
    apiUrl: process.env.BROADCAST_API_URL,
    token: process.env.BROADCAST_TOKEN,
    // Optional: These can be set here as defaults or configured in the admin UI
    fromAddress: '[email protected]',
    fromName: 'Your Newsletter',
    replyTo: '[email protected]',
  },
}

Note: Settings configured in the Payload admin UI take precedence over these config values. The config values serve as defaults when settings haven't been configured yet.

TypeScript

The plugin is fully typed. Import types as needed:

import type { 
  NewsletterPluginConfig,
  Subscriber,
  EmailProvider 
} from 'payload-plugin-newsletter/types'

Customization

Custom Fields

Add custom fields to the subscribers collection:

newsletterPlugin({
  fields: {
    additional: [
      {
        name: 'company',
        type: 'text',
        label: 'Company Name',
      },
      {
        name: 'role',
        type: 'select',
        options: ['developer', 'designer', 'manager'],
      },
    ],
  },
})

Custom Email Templates

Override the default email templates:

import { WelcomeEmail } from './emails/Welcome'

newsletterPlugin({
  templates: {
    welcome: WelcomeEmail,
  },
})

Extending the Broadcasts Collection (v0.15.0+)

You can extend the Broadcasts collection with additional fields and custom email-compatible blocks:

import type { Block } from 'payload'

const customBlock: Block = {
  slug: 'product-spotlight',
  labels: { singular: 'Product Spotlight', plural: 'Product Spotlights' },
  fields: [
    { name: 'product', type: 'relationship', relationTo: 'products', required: true },
    { name: 'description', type: 'textarea' }
  ]
}

newsletterPlugin({
  // ... existing config
  customizations: {
    broadcasts: {
      additionalFields: [
        {
          name: 'slug',
          type: 'text',
          required: true,
          admin: { position: 'sidebar' }
        }
      ],
      customBlocks: [customBlock], // Processed server-side for email compatibility
      fieldOverrides: {
        content: (defaultField) => ({
          ...defaultField,
          admin: {
            ...defaultField.admin,
            description: 'Custom description'
          }
        })
      },
      // Email preview customization (v0.20.0+)
      emailPreview: {
        // Disable default email template wrapping
        wrapInTemplate: false,
        
        // Or provide a custom wrapper function
        customWrapper: async (content, { subject, preheader }) => {
          return `
            <div class="my-custom-template">
              <h1>${subject}</h1>
              ${preheader ? `<p class="preheader">${preheader}</p>` : ''}
              <div class="content">${content}</div>
            </div>
          `
        }
      }
    }
  }
})

Note: Custom blocks are processed server-side to ensure email compatibility and prevent Next.js serialization errors.

Email Preview Customization (v0.20.0+)

The plugin now supports full customization of email preview rendering. This is useful when you have custom email templates and want the preview to match what's actually sent.

Disable Default Template Wrapping

If you're using your own email template system, you can disable the default template wrapping:

newsletterPlugin({
  customizations: {
    broadcasts: {
      emailPreview: {
        wrapInTemplate: false // Show raw HTML without email template
      }
    }
  }
})

Custom Email Wrapper

Provide your own wrapper function to match your email service's template:

newsletterPlugin({
  customizations: {
    broadcasts: {
      emailPreview: {
        customWrapper: async (content, { subject, preheader }) => {
          // Return your custom email template
          return `
            <!DOCTYPE html>
            <html>
              <head>
                <title>${subject}</title>
                <!-- Your custom styles -->
              </head>
              <body>
                <div class="preheader">${preheader}</div>
                ${content}
                <!-- Your footer -->
              </body>
            </html>
          `
        }
      }
    }
  }
})

Advanced: Using with React Email

If you're using React Email for templates, you can integrate it with the preview:

import { render } from '@react-email/render'
import { MyEmailTemplate } from './emails/MyEmailTemplate'

newsletterPlugin({
  customizations: {
    broadcasts: {
      emailPreview: {
        customWrapper: async (content, { subject, preheader }) => {
          return await render(
            <MyEmailTemplate 
              subject={subject}
              preheader={preheader}
              content={content}
            />
          )
        }
      }
    }
  }
})

This ensures your preview exactly matches what subscribers will see.

For complete extensibility documentation, see the Extension Points Guide.

Troubleshooting

Common Issues

"Already subscribed" error

  • The email already exists in the subscribers collection
  • Check the admin panel to manage existing subscribers

Magic links not working

  • Ensure JWT_SECRET is set in your environment variables
  • Check that the magicLinkPath matches your frontend route

Emails not sending

  • Verify your API keys are correct
  • Check the email provider's dashboard for errors
  • Ensure from address is verified with your provider

Security

Access Control

The plugin implements proper access control for all operations:

  • Subscriber data: Users can only access and modify their own data via magic link authentication
  • Newsletter settings: Only admin users can modify email provider settings and configurations
  • API endpoints: All endpoints respect Payload's access control rules

Custom Admin Check

The plugin supports multiple admin authentication patterns out of the box:

  • user.roles.includes('admin') - Role-based
  • user.isAdmin === true - Boolean field
  • user.role === 'admin' - Single role field
  • user.admin === true - Admin boolean

If your setup uses a different pattern, configure a custom admin check:

newsletterPlugin({
  access: {
    isAdmin: (user) => {
      // Your custom logic
      return user.customAdminField === true
    }
  },
  // ... other config
})

Best Practices

  • Always use environment variables for sensitive data (API keys, JWT secrets)
  • Enable double opt-in for GDPR compliance
  • Configure allowed domains to prevent spam subscriptions
  • Set reasonable rate limits for subscriptions per IP

Migration Guide

Coming from another newsletter system? The plugin stores subscribers in a standard Payload collection, making it easy to import existing data:

// Example migration script
const existingSubscribers = await getFromOldSystem()

for (const subscriber of existingSubscribers) {
  await payload.create({
    collection: 'subscribers',
    data: {
      email: subscriber.email,
      name: subscriber.name,
      subscriptionStatus: 'active',
      // Map other fields as needed
    },
  })
}

Contributing

We welcome contributions! Please see our feedback and contribution guide.

Release Process

This project uses a developer-controlled release process:

  • Version bumps happen locally - You control when and what type
  • CI/CD publishes automatically - When it detects a version change
  • No bot commits - Your local repo stays in sync

See Release Documentation for details.

License

MIT