How Ghost Newsletters and Automated Emails Work: A Technical Architecture Guide

Ghost's newsletter system uses a four-layer architecture—data models, service logic, REST APIs, and an email pipeline—to create verified newsletters, manage member subscriptions, and deliver automated email campaigns through configurable mail transports.

Ghost's newsletter functionality in the TryGhost/Ghost repository provides a full-stack solution for content creators to build and distribute email campaigns directly from their publishing platform. Understanding how Ghost newsletters and automated emails work requires examining the interaction between data persistence layers, business logic services, and the email delivery pipeline that renders and transmits each campaign to subscribers.

The Four-Layer Architecture

Ghost implements newsletters through four distinct layers that handle everything from database schema to mail delivery.

Data Models and Relationships

The foundation resides in ghost/core/core/server/models/newsletter.js, which defines the Newsletter record with properties including name, status, sender_email, sender_reply_to, schedule, and brand colors. The system maintains a many-to-many relationship between newsletters and members through the member_newsletters join table, implemented in ghost/core/core/server/models/member-newsletter.js. This junction table enables individual members to subscribe to multiple newsletters while allowing each newsletter to maintain its own subscriber list.

Service Layer Business Logic

All domain logic lives in ghost/core/core/server/services/newsletters/newsletters-service.js. The NewslettersService class encapsulates creation, editing, validation, email-address verification, and bulk subscription workflows. This service acts as the single source of truth for newsletter operations, isolating business rules from API controllers and database models.

REST API Endpoints

The API layer exposes endpoints through ghost/core/core/server/api/endpoints/newsletters.js for administrative operations and ghost/core/core/server/api/endpoints/newsletters-public.js for public access. These controllers validate requests, enforce permissions, and delegate execution to the service layer. Key endpoints include POST /newsletters for creation and GET /newsletters/verify/:token for address verification.

Email Pipeline and Delivery

The delivery mechanism renders Handlebars templates from ghost/core/core/server/services/mail/templates/newsletter.html, constructs MIME payloads, and transmits messages via GhostMailer. This pipeline integrates with Nodemailer-compatible transports including SMTP and SendGrid, handling both HTML and plain-text fallback rendering for each recipient.

Creating a Newsletter with Verification

Creating a newsletter initiates a multi-step workflow that enforces rate limits and validates sender identities before activation.

When the Admin UI posts to POST /newsletters, the controller invokes NewslettersService.add, which performs the following operations:

  1. Enforces plan limits via limitService to check against the newsletters resource cap
  2. Prepares attributes using prepAttrsForEmailVerification to strip unverified sender_email or sender_reply_to values
  3. Persists the record with a calculated sort_order for display organization
  4. Handles opt-in subscriptions when opt_in_existing: true by calling MemberModel.fetchAllSubscribed and executing subscribeMembersById within the same database transaction
  5. Triggers verification emails via respondWithEmailVerification, which invokes sendEmailVerificationMagicLink for each address requiring confirmation
await api.newsletters.add({
    newsletters: [{
        name: 'Weekly Update',
        status: 'active',
        sender_email: 'news@myblog.com',
        sender_reply_to: 'support',
        opt_in_existing: true
    }]
});

Ghost implements sender verification through cryptographically secure magic links to prevent email spoofing and ensure deliverability.

The prepAttrsForEmailVerification method checks each sender address against the Email Address Service (emailAddressService.service.validate). Addresses requiring verification are removed from the model attributes and added to an emailsToVerify collection.

The sendEmailVerificationMagicLink function generates a one-time MagicLink token encoding {id, property, value} and dispatches a verification message through ghostMailer. When administrators click the verification link (formatted as /settings/newsletters/?verifyEmail=…), the endpoint routes to NewslettersService.verifyPropertyUpdate, which validates the token and commits the pending property change to the database, marking it in meta.email_verified.

// POST /newsletters/verify/:token
const updated = await newslettersService.verifyPropertyUpdate(token);
// `updated.meta.email_verified` now contains the verified property name

Managing Member Subscriptions

Member relationships are maintained through the member_newsletters join table, enabling granular subscription management independent of general site membership.

When creating a newsletter with opt_in_existing: true, the service queries existing subscribed members via MemberModel.fetchAllSubscribed, then bulk-inserts associations through newsletter.subscribeMembersById. This operation executes within a database transaction to ensure consistency between the newsletter creation and subscription records. The join table structure allows members to selectively unsubscribe from individual newsletters while remaining subscribed to others.

Rendering and Sending Emails

The actual email transmission combines template rendering with personalized delivery logic.

Ghost reads the Handlebars template from ghost/core/core/server/services/mail/templates/newsletter.html, injecting variables such as {{blog.title}}, {{newsletter.interval}}, and featured post content. The rendered HTML (plus optional text fallback) passes to GhostMailer, which iterates through the newsletter's member associations to assemble personalized messages including unique unsubscribe links.

The mailer dispatches asynchronously through the configured transport layer, logging each transmission and outputting text versions to the console in non-production environments (as seen in newsletters-service.js lines 46-48).

Automation and Scheduling

Automated newsletter delivery relies on Ghost's generic job scheduler rather than cron-specific implementations.

Each newsletter stores an interval field (daily, weekly, or monthly) and a calculated send_at timestamp. The background worker process (core/server/services/worker/worker.js) polls active newsletters where status = 'active' and send_at <= now, then triggers the same rendering and mailer pipeline used for manual sends. This design allows newsletters to function as standard job types within Ghost's unified queue system.

// Simplified scheduler logic
const newsletters = await newslettersService.getAll({status: 'active'});
for (const nl of newsletters) {
    if (nl.send_at <= Date.now()) {
        await sendNewsletter(nl); // renders template + GhostMailer loop
    }
}

Security and Permissions

Access control protects newsletter operations through Ghost's permission framework.

All CRUD endpoints specify permissions: true, requiring authenticated admin access. The verification link route (verifyPropertyUpdate) specifically requires "edit" permissions on newsletters, ensuring only authorized administrators can approve sender address changes. The Email Address Service enforces additional domain whitelists and validation rules before permitting verification emails to transmit.

Summary

  • Ghost newsletters operate through four layers: data models (newsletter.js, member-newsletter.js), service logic (newsletters-service.js), REST APIs (newsletters.js), and the email pipeline (GhostMailer).
  • Sender verification uses magic links generated by sendEmailVerificationMagicLink and validated through verifyPropertyUpdate to ensure email deliverability.
  • Member subscriptions utilize the member_newsletters join table, with bulk opt-in support via fetchAllSubscribed and subscribeMembersById.
  • Email rendering uses Handlebars templates (newsletter.html) processed through GhostMailer with personalized unsubscribe links for each recipient.
  • Automated scheduling relies on Ghost's job worker polling send_at timestamps and status fields to trigger delivery.
  • All operations enforce permissions through the permissions: true API framework, with specific edit rights required for address verification.

Frequently Asked Questions

How does Ghost verify newsletter sender email addresses?

Ghost verifies sender addresses through a magic-link system implemented in newsletters-service.js. When creating or updating a newsletter, the prepAttrsForEmailVerification method checks addresses against the Email Address Service. Unverified addresses trigger sendEmailVerificationMagicLink, which generates a single-use token encoding the newsletter ID and pending value. Administrators must click the verification link to invoke verifyPropertyUpdate, which commits the address to the database only after confirmation.

What happens when I check "opt in existing members" when creating a newsletter?

When opt_in_existing: true is passed to NewslettersService.add, the system queries all currently subscribed members via MemberModel.fetchAllSubscribed, then bulk-inserts records into the member_newsletters join table within the same database transaction. This immediately subscribes existing members to the new newsletter without requiring them to opt-in manually.

How does Ghost schedule automatic newsletter delivery?

Ghost stores interval (daily/weekly/monthly) and send_at fields on each newsletter record. A background job worker polls for newsletters where status = 'active' and the current time exceeds send_at. When conditions match, the worker invokes the standard email pipeline—rendering the Handlebars template and dispatching through GhostMailer—then recalculates the next send date based on the interval.

Can members subscribe to multiple newsletters independently?

Yes. Ghost uses the member_newsletters join table (member-newsletter.js) to maintain many-to-many relationships between members and newsletters. This architecture allows members to subscribe or unsubscribe from individual newsletters while maintaining their general site membership, with each subscription tracked as a separate record in the junction table.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →