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

@growth-labs/mailer

v0.4.7

Published

Queue-based email sending engine for Astro + Cloudflare. Newsletter subscriptions with double opt-in, campaign sending, open/click tracking, unsubscribe management. Uses Cloudflare Queues for async delivery.

Readme

@growth-labs/mailer

Queue-based email sending engine for Astro + Cloudflare. Newsletter subscriptions with double opt-in, campaign sending, open/click tracking, unsubscribe management. Uses Cloudflare Queues for async delivery.

"The engine, not the driver." This package sends emails. What to send and to whom is the consumer's concern (e.g. @fulcrum/dispatch layers editorial intelligence on top).

If you only need immediate transactional delivery from a Worker with no subscriber state or queueing, use @growth-labs/email instead.

Astro 6 note: injected routes read D1 and Queue bindings from cloudflare:workers env; no locals.runtime shim is required.

When to use this vs @growth-labs/email

Use @growth-labs/mailer (this package) for newsletter and campaign workflows. Use @growth-labs/email for transactional one-off sends. See the @growth-labs/email README for the full distinction.

Config

import mailer from '@growth-labs/mailer'

mailer({
  senderName: 'FEDweek',
  fromAddress: '[email protected]',
  replyTo: '[email protected]',
  d1Binding: 'SITE_DB',
  queueBinding: 'EMAIL_QUEUE',
  turnstileSiteKey: '...',
  turnstileSecretKey: '...',                     // Via Cloudflare Secrets
  doubleOptIn: true,
  topics: ['daily-digest', 'breaking-news'],     // Optional topic-based subscriptions
  signingSecret: '...',                          // HMAC for unsubscribe tokens
  siteUrl: 'https://fedweek.com',
  brand: {
    logoUrl: 'https://media.fedweek.com/logos/email.png',
    primaryColor: '#1a365d',
    accentColor: '#e53e3e',
    footerText: '© FEDweek. All rights reserved.',
  },
  analyticsEnabled: true,                        // Emit email events to @growth-labs/analytics
  analyticsBinding: 'ANALYTICS',                 // WAE binding used when analytics is enabled
  webhookSignature: {
    enabled: true,
    secret: import.meta.env.MAILER_WEBHOOK_SECRET,
  },
})

Production patterns

from vs replyTo. A common pattern is fromAddress: '[email protected]' paired with replyTo: '[email protected]'. The from lives on a domain you've verified with Cloudflare Email Sending (typically a mail. subdomain); replyTo points at a real human inbox readers can write back to. replyTo is also a per-message override on sendTransactional and a per-site field on SiteMailerConfig (realm pattern, since 0.4.0).

Don't bake fallbacks for fromAddress. The schema requires a valid email (z.string().email()) and ships no default — that's intentional. A consumer-side fallback like process.env.MAILER_FROM_ADDRESS ?? '[email protected]' is a foot-gun: if the env var is missing at deploy time the fallback domain probably isn't verified with Cloudflare Email Sending, every send returns unauthorized_sender, and the queue retries until the budget kills the message. Fail loudly at build/deploy when the env var is missing instead — the schema can't catch your fallback.

What It Injects

Routes:

  • POST /api/newsletter/subscribe — Turnstile-protected subscription
  • GET /api/newsletter/confirm?token=... — Double opt-in confirmation
  • GET /api/newsletter/unsubscribe?token=... — One-click unsubscribe
  • GET/POST /email/preferences?token=... — Preference center page
  • POST /api/email/webhook — ESP webhook receiver (delivery, bounce, complaint), optional HMAC signature verification
  • GET /api/email/open/:trackingId — Open tracking pixel
  • GET /api/email/click/:trackingId — Click tracking redirect

Components:

  • <SubscribeForm /> — Newsletter signup form with Turnstile
  • <PreferenceCenter /> — Topic subscription management

D1 Tables (prefixed gl_)

  • gl_subscribers — subscriber records (email, name, status, topics, confirmed_at)
  • gl_email_sends — send log (status: queued → sent → delivered → opened → clicked)

Wrangler Bindings

[[d1_databases]]
binding = "SITE_DB"
database_id = "..."

[[queues.producers]]
binding = "EMAIL_QUEUE"
queue = "email-sends"

[[queues.consumers]]
queue = "email-sends"
max_batch_size = 10

[[analytics_engine_datasets]]
binding = "ANALYTICS"
dataset = "your_analytics_dataset"

Email Sending Flow

  1. Consumer calls sendTransactional(), sendCampaign(), or sendDigest() → message enqueued
  2. Queue consumer dequeues → sends via Cloudflare Email Sending with retry on retryable failures
  3. Status updated in gl_email_sends: queued → sent → delivered/bounced
  4. Open/click tracking updates status further

Key Patterns

  • Virtual module: virtual:growth-labs/mailer/config
  • Runtime bindings: import { env } from 'cloudflare:workers'
  • Status never downgrades (sent → delivered → opened → clicked)
  • List-Unsubscribe header on every email (RFC 8058)
  • Turnstile on subscribe endpoint (+ IP rate limiting as backup)
  • Provider failover logged for observability
  • WAE events via the configured Analytics Engine binding — fire-and-forget, non-blocking, and compatible with bindings whose writeDataPoint() returns void
  • .astro component files ship as source, not compiled

Analytics Events

When analyticsEnabled is true and the configured analyticsBinding exists, the package writes WAE data points for newsletter_subscribed, newsletter_confirmed, newsletter_unsubscribed, newsletter_opened, newsletter_clicked, newsletter_delivered, newsletter_bounced, newsletter_complained, newsletter_sent, and newsletter_send_failed.