Skip to main content

Maquina Newsletters — Maquina

A mountable Rails 8 engine for drafting, approving, scheduling, and batch-sending HTML newsletters. Action Text editing, an approval workflow, and background batch delivery.

A mountable Rails 8 engine for drafting, approving, scheduling, and batch-sending HTML newsletters. Compose with Action Text, gate sends behind an approval step, schedule deliberately, and let a background job deliver in batches — all from a backstage area inside your own app.


What Is This?

Maquina Newsletters is a Rails engine you mount under a backstage path (e.g. /backstage/newsletters). It gives you a complete newsletter lifecycle without bringing in an external service:

  • Draft and edit with the Action Text rich-text editor — Trix or Lexxy, your choice
  • Image attachments and embeds via Active Storage
  • Approval workflow — a newsletter can’t be sent straight from a draft
  • Deliberate scheduling — pick the date, time, and batch size in one explicit step
  • Batch sending — split delivery across days, or send to everyone at once
  • Test sends to any address, plus a send-now override
  • Per-issue exclusion list to drop specific recipients

The engine renders its own UI (themed with maquina_components) and resolves recipients from a model and scope you configure. Authentication stays in your hands — the engine inherits from a base controller you point it at.


Requirements

  • Rails 8
  • Active Storage and Action Text configured in the host app
  • image_processing (~> 2.0) plus an image processor — ruby-vips (recommended) or mini_magick
  • A system image library: libvips (recommended) or ImageMagick
    • macOS: brew install vips (or brew install imagemagick)
    • Debian/Ubuntu: apt-get install libvips (or apt-get install imagemagick)
  • tailwindcss-rails (engine UI theming) and maquina_components (host theming)
  • lexxy — optional, for the Lexical-based editor (Rails 8.1+ can auto-configure it)

Quick Start

1. Add the Gem

# Gemfile
gem "maquina_newsletters", "~> 1.5"
bundle install

2. Mount the Engine

# config/routes.rb
mount MaquinaNewsletters::Engine => "/backstage/newsletters"

3. Run the Installer

bin/rails generate maquina_newsletters:install
bin/rails db:migrate

The installer sets up the engine’s migrations and, if they aren’t already present, runs active_storage:install and action_text:install for you.

4. Add Image Processing

# Gemfile
gem "image_processing", "~> 2.0"
gem "ruby-vips" # or: gem "mini_magick"

Then bundle install and install the system library (see Requirements).

5. Wire Up Tailwind

/* app/assets/tailwind/application.css */
@import "tailwindcss";
@import "../builds/tailwind/maquina_newsletters";
bin/rails tailwindcss:build   # or tailwindcss:watch in development

Keep app/assets/builds/* in .gitignore and rebuild on each machine.

6. Set the Mailer Host

So image URLs in delivered emails are absolute:

# config/environments/production.rb
config.action_mailer.default_url_options = { host: "newsletters.example.com" }

Configuration

Create an initializer to tell the engine who receives newsletters and how it’s protected:

# config/initializers/maquina_newsletters.rb
MaquinaNewsletters.configure do |config|
  # Recipient resolution — which records receive a newsletter.
  config.recipient_model      = "User"          # constantized at use-time
  config.recipient_scope      = :active         # a scope returning a relation
  config.recipient_email_attr = :email_address  # the email column

  # Base controller — see "Authentication" below.
  config.base_controller_class = "BackstageController"

  # Optional HTTP Basic Auth (off by default)
  config.http_basic_auth_enabled  = true
  config.http_basic_auth_user     = ENV["NEWSLETTERS_USER"]
  config.http_basic_auth_password = ENV["NEWSLETTERS_PASSWORD"]
end

If the initializer is absent, the defaults are:

Setting Default
recipient_model "User"
recipient_scope :active
recipient_email_attr :email_address
base_controller_class "ActionController::Base"
HTTP Basic Auth disabled

Authentication

The engine does not provide authentication — that’s the host app’s job. Every engine controller inherits from a base controller you configure by name:

config.base_controller_class = "BackstageController"

Point it at an already-authenticated controller in your app (session checks, etc.) and every engine route is protected automatically.

For apps whose base controller doesn’t authenticate, the engine ships an optional HTTP Basic Auth fallback:

  • Enabled with credentials — challenges with HTTP Basic Auth
  • Enabled without credentials — fails closed (401 on every request)
  • Disabled — no built-in auth; relies on the base controller

Don’t stack both methods — pick one.


The Newsletter Lifecycle

A newsletter moves through four states:

State What happens
Draft Create and edit content (subject + Action Text body). Saving creates a draft; no send time is set.
Approved Approve a draft when it’s ready. You can’t send from a draft.
Scheduled On an approved issue, set the send timing and batch size.
Sending → Sent A background job delivers. A sending/sent issue can’t be edited.

You can move backward too: Back to draft (from approved/scheduled/sent) and Unschedule (from scheduled back to approved).

Scheduling

The schedule form appears on an approved issue and takes three inputs:

  • Date — date picker, today onward (no past dates)
  • Time — 8:00 AM to 8:00 PM in 30-minute increments
  • Batch size — recipients per batch. 0 sends to everyone at once; a positive number splits the send across days, one batch per day.

If the chosen date/time has already passed, it auto-rolls forward to the next 30-minute slot and the confirmation says so. Once scheduled, a read-only summary shows Recipients / Scheduled at / Batch size / Sent at.

Send Now & Test Send

  • Send now — an overflow (⋮) action that delivers immediately to all recipients behind a confirmation, bypassing scheduling.
  • Test send — available while drafting/approving/scheduling. Sends exactly one email to any address you type, ignoring batch size and schedule, without changing the issue’s state. Ideal for preview validation.

Recipients

Recipients are resolved at send time from your configured model and scope (e.g. User.active), minus the per-issue exclusion list. The resulting addresses are downcased, de-duplicated, and sorted for stable batching.


Editors

The host chooses the Action Text editor via config.action_text.editor:

  • :trix — Rails default, no extra setup
  • :lexxy — Lexical-based; install the lexxy gem and wire up its JS/CSS

For Lexxy with importmaps:

# config/importmap.rb
pin "lexxy", to: "lexxy.js"
pin "@rails/activestorage", to: "activestorage.esm.js"
// app/javascript/application.js
import * as ActiveStorage from "@rails/activestorage"
import "lexxy"
ActiveStorage.start()
<%# in your layout, after the CSS build %>
<%= stylesheet_link_tag "lexxy" %>

On Rails 8.1, installing the lexxy gem auto-sets config.action_text.editor = :lexxy; set it to :trix to override.


Next Steps