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

@payload-solutions/plugin-emails

v0.1.0

Published

Code-defined transactional emails for Payload CMS with admin-editable copy, typed send API, preview and test-send.

Readme

@payload-solutions/plugin-emails

Code-defined transactional emails for Payload CMS. You declare each email once in TypeScript — its key, the input callers pass, the variables editors may use, and default copy. The plugin keeps one document per email in the admin, where non-developers edit the subject and body, preview it with sample data, send themselves a test, and switch non-critical emails off. Application code sends with one typed call.

await payload.emails.send('welcome', { input: { user, url } })

Ported from the Klick17 WordPress K7P_Email framework and rebuilt on Payload primitives: collections, versions, localization, Lexical, the email adapter, jobs, and generate:types.

Install

pnpm add @payload-solutions/plugin-emails

Peer dependencies: payload ^3.88, @payloadcms/richtext-lexical, @payloadcms/ui, react, react-dom. Rendering uses @react-email/components and @react-email/render, which ship with the plugin.

1. Declare an email

// src/emails/welcome.ts
import { defineEmail, populate } from '@payload-solutions/plugin-emails'

export const welcome = defineEmail({
  slug: 'welcome',
  label: 'Welcome',
  description: 'Sent once after a user creates an account.',
  trigger: 'users afterChange hook (operation: create)',
  group: 'Auth',
  audience: 'user',

  // What callers pass. Payload fields → a typed `input` in payload-types.ts.
  inputSchema: [
    { name: 'user', type: 'relationship', relationTo: 'users', required: true },
    { name: 'url', type: 'text', required: true },
  ],

  // What editors may write in the copy.
  variables: {
    'user.name': { description: 'Display name', example: 'Ada Lovelace' },
    'user.email': { example: '[email protected]' },
    url: { type: 'url', example: 'https://app.example.com/dashboard' },
  },

  // input → variables. Typed both ways.
  resolve: async ({ input, payload }) => {
    const user = await populate(payload, 'users', input.user) // id or document
    return { 'user.name': user.name || user.email, 'user.email': user.email, url: input.url }
  },

  to: ({ variables }) => variables['user.email'],

  // Seeded into the document on first start; also the fallback if the document is missing.
  defaults: {
    subject: 'Welcome to {{site.name}}, {{user.name}}',
    preheader: 'Your account is ready.',
    body: `
Hi {{user.name}},

Thanks for creating an account on [{{site.name}}]({{site.url}}).

<Button label="Open your dashboard" url="{{url}}" />
`,
  },

  sample: { url: 'https://app.example.com/dashboard', user: 'some-user-id' },
})

2. Register the plugin

// payload.config.ts
import { emailsPlugin } from '@payload-solutions/plugin-emails'
import { welcome, passwordReset } from './emails'

plugins: [
  emailsPlugin({
    emails: [welcome, passwordReset],
    settings: { adminRecipients: ['[email protected]'], mediaCollection: 'media' },
    log: { enabled: true, retentionDays: 90 },
  }),
]

Then payload generate:types and payload generate:importmap.

3. Send

const result = await payload.emails.send('welcome', {
  input: { user, url },     // typed; unknown keys and missing fields are compile errors
  locale: req.locale,       // optional
  to: '[email protected]', // optional
  req,                      // optional: transaction / locale inheritance
})
// { status: 'sent' | 'skipped' | 'queued' | 'failed', reason?, messageId?, logId? }

await payload.emails.render('welcome', { input })  // { subject, html, text, to, variables }
await payload.emails.sync()                        // re-seed / refresh; runs on init
await payload.emails.orphans()                     // documents no longer defined in code

send returns a status for expected outcomes (disabled, no recipient, adapter failure) and throws only for programmer errors (unknown slug, invalid input).

Templates

The design lives in code, as a React Email component. It owns the branding — colours, spacing, the wordmark, the shell — so every email looks the same and no editor can drift it.

import { Body, Container, Head, Html, Section, Text } from '@react-email/components'
import type { EmailTemplate } from '@payload-solutions/plugin-emails'

export const BrandTemplate: EmailTemplate = ({ children, footer, settings, subject }) => (
  <Html>
    <Head />
    <Body style={{ backgroundColor: '#0b0b0c', fontFamily: 'Inter, sans-serif' }}>
      <Container style={{ maxWidth: 600 }}>
        <Section style={{ backgroundColor: '#fff', borderRadius: 12, padding: 40 }}>{children}</Section>
        {footer}
      </Container>
    </Body>
  </Html>
)

// Make the editors' copy match the template.
BrandTemplate.styles = { text: { color: '#1a1a1a', fontSize: '15px' }, button: { backgroundColor: '#ff5a1f' } }
emailsPlugin({ emails, templates: { default: BrandTemplate } })

children is the editor's copy, already rendered to React Email elements (paragraphs, headings, lists, links, buttons) with {{variables}} filled in. A definition can pick a different one with template: 'receipt'. Both the HTML and the plain-text part come from the same tree, so they never disagree.

What you get in the admin

  • Transactional Emails collection — one document per definition, with drafts. Editors change subject, preheader, body (Lexical with a Button block), recipients and the enabled switch. Unknown {{tokens}} — and tokens broken up by bold/italic — fail validation with the list of allowed variables.
  • Sidebar variable chips (click to copy) and an About this email panel with the key, trigger, audience and an orphan badge.
  • Preview & test tab — a form built from the email's inputSchema (relationships offer real documents to pick from; there's a JSON escape hatch), the rendered email in a sandboxed iframe (desktop/mobile, HTML/plain text, draft/published, locale), the resolved recipients and variables, and a test send to any address.
  • Email Settings global — sender and reply-to, admin recipients, site name and URL, the footer, and a Template tab that previews the active template and points editors at their developers for design changes.
  • Email Log collection (opt-in) — every send with status, recipient and reason.

How code and database stay in sync

Code owns metadata (label, description, trigger, group, audience, required, the variable manifest); the database owns copy. On init the plugin creates a published document for every new definition from defaults, refreshes stored metadata whose hash changed, renames documents listed in previousSlugs, and flags documents whose key is gone as orphans. Copy is never overwritten by a deploy, and refreshes never create versions or drafts.

Typing

The plugin adds a typescript.schema hook, so payload generate:types emits:

export interface Config {
  emails: { welcome: EmailWelcome; 'password-reset': EmailPasswordReset }
}
export interface EmailWelcome {
  input: { user: number | User; url: string }
  variables: { 'user.name': string; 'user.email': string; url: string }
}

payload.emails.send and defineEmail derive from that, the same way Payload types jobs tasks. Before the first generation everything falls back to permissive types, so a fresh project still compiles.

Options

| Option | Default | | | --- | --- | --- | | emails | — | Required. The definitions. | | collectionSlug | transactional-emails | | | settings | {} | false to skip the global; adminRecipients, mediaCollection, slug. | | log | off | { enabled, slug, retentionDays, storeHtml, storeVariables, includeTests }. | | queue | off | { enabled, default, queue, retries } — deliver through Payload Jobs. | | templates | { default: DefaultTemplate } | React Email components; pick per definition with template. | | editor | email-safe Lexical | Replace the editor used for body. | | globalVariables | site/support/year | Variables available in every email. | | versions | { drafts: true } | | | seed | always | development or false. | | validateInput | development | always / never. | | access | admin users | read / update / delete. | | hooks | — | shouldSend, beforeRender, beforeSend, afterSend. |

Development

pnpm install
pnpm dev              # dev app on http://localhost:3300/admin ([email protected] / test)
pnpm test:unit        # interpolation, definitions, validation
pnpm test:int         # real Payload + SQLite: seeding, sending, endpoints, orphans
pnpm build            # dist/ via tsc + swc
pnpm generate:types   # regenerate dev/payload-types.ts

The dev app (dev/) registers three example emails in dev/emails.ts and logs every send to stdout through dev/helpers/testEmailAdapter.ts. Creating a user in the admin fires the welcome and admin-notification emails.

MIT © Payload Solutions. Not affiliated with Payload CMS.