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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xtr-dev/payload-mailing

v0.6.0

Published

Template-based email system with scheduling and job processing for PayloadCMS

Readme

@xtr-dev/payload-mailing

npm version

Template-based email for PayloadCMS 3.x — templates, layouts, scheduling, and job-queue processing, all through Payload collections you already know.

⚠️ Pre-release (v0.x). Breaking changes may occur before v1.0.0.

Install

pnpm add @xtr-dev/payload-mailing

Quick Start

Add the plugin plus an email adapter:

import { buildConfig } from 'payload'
import { mailingPlugin } from '@xtr-dev/payload-mailing'
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'

export default buildConfig({
  email: nodemailerAdapter({ defaultFromAddress: '[email protected]', transport: { /* SMTP */ } }),
  plugins: [
    mailingPlugin({
      defaultFrom: '[email protected]',
      collections: {
        // Required — collections deny all access by default. See "Access Control".
        emails: { access: { read: ({ req: { user } }) => Boolean(user) } },
        templates: { access: { read: ({ req: { user } }) => Boolean(user) } },
      },
    }),
  ],
})

This adds two collections under a Mailing admin group: email-templates (author templates) and emails (delivery + status tracking).

Access Control

The mailing collections deny every operation by default. Payload's built-in default grants access to any authenticated user, which in an app with front-end/non-admin users would expose email content, recipients, and templates. Until you grant access explicitly, no one can read or manage these collections via the REST/GraphQL API or admin panel — but sending still works, since the plugin sends through Payload's Local API (which bypasses access control).

Grant access via the collection overrides. Your functions are merged on top of the deny-all default, so any operation you don't set stays denied:

mailingPlugin({
  collections: {
    emails: {
      access: {
        read:   ({ req: { user } }) => user?.role === 'admin',
        create: ({ req: { user } }) => Boolean(user),
        update: ({ req: { user } }) => user?.role === 'admin',
        delete: ({ req: { user } }) => user?.role === 'admin',
      },
    },
    // templates: { access: { … } }
  },
})

access is the standard Payload access control API. The same override object also accepts custom fields, hooks, admin, etc.

Sending Email

import { sendEmail } from '@xtr-dev/payload-mailing'

// From a template
await sendEmail(payload, {
  template: { slug: 'welcome-email', variables: { firstName: 'John' } },
  data: {
    to: '[email protected]',
    scheduledAt: new Date(Date.now() + 3600_000), // optional: send later
    priority: 1,                                   // optional: 1 = highest
  },
})

// Or with your own content
await sendEmail(payload, {
  data: { to: '[email protected]', subject: 'Hi', html: '<h1>Hi</h1>' },
})

Emails are queued and sent in the background (see Jobs). Pass processImmediately: true to send synchronously.

Templates

Author templates in the admin (Mailing → Email Templates): a slug, subject, rich-text content, and optional declared variables. Reference data with {{ }}, e.g. Hello {{ user.name }}! or {{ createdAt | formatDate: "long" }}.

  • Engines — set templateEngine to liquidjs (default), mustache, or simple, or supply templateRenderer: (tpl, vars) => string for your own.
  • Escaping — HTML-body variables are HTML-escaped by default (opt out with LiquidJS {{ x | raw }} / Mustache {{{ x }}}); subject and text are verbatim.
  • Required variables — mark declared variables Required and a send missing one is rejected before it's queued. Opt-in; declare nothing to skip checks.

Render without sending: renderTemplate(payload, slug, vars){ html, text, subject }.

Layouts

Define reusable wrappers once and inject a template's body at {{ content }}:

mailingPlugin({
  layouts: {
    branded: { html: `<html><body><main>{{ content }}</main></body></html>` },
  },
  defaultLayout: 'branded', // applied to templates that don't pick their own
})

Templates then get a Layout select (with Use default / None). Layout variables are always HTML-escaped; content is injected without double-encoding. Fully opt-in — with no layouts, templates render exactly as before.

Jobs

The plugin registers its processing job automatically — nothing to add. Sending (or creating an emails doc) queues a background job that honors scheduledAt and retries failures. You just need a job runner: either Payload's jobs.autoRun cron, or call processEmails(payload) yourself (drains up to 50 due emails per call, highest priority first). Emails track a status of pending → processing → sent / failed.

Options

| Option | Description | | --- | --- | | defaultFrom / defaultFromName | Default sender for emails that don't set one. | | retryAttempts / retryDelay | Retry count and delay (ms) for failed sends. | | queue | Job queue name (default 'default'). | | templateEngine | 'liquidjs' | 'mustache' | 'simple'. | | templateRenderer | Custom (tpl, vars) => string \| Promise<string>. | | layouts / defaultLayout | Named layout wrappers and the default one. | | adminPreview | Live in-admin render preview; true by default. Set false to skip it (and the payload generate:importmap step). | | collections | Rename ('slug') or override ({ access, fields, … }) the emails/templates collections. | | beforeSend | (options, email) => options hook to mutate the send just before delivery. |

API

| Export | Purpose | | --- | --- | | sendEmail(payload, options) | Queue (or immediately send) a template/direct email. | | renderTemplate(payload, slug, vars) | Render { html, text, subject } without sending. | | processEmails(payload) | Process up to 50 due-pending emails. | | retryFailedEmails(payload) | Re-queue failed emails. | | getMailing(payload) | Get the mailing context (service, config, slugs). |

sendEmail<Email>(…) is generic over your generated Email type for type-safe custom fields.

Requirements

PayloadCMS ^3.0.0 · Node.js ^18.20.2 || >=20.9.0

Contributing

Issues and PRs welcome at the repository; see DEVELOPMENT.md for local setup. MIT licensed.