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) ormini_magick- A system image library:
libvips(recommended) or ImageMagick- macOS:
brew install vips(orbrew install imagemagick) - Debian/Ubuntu:
apt-get install libvips(orapt-get install imagemagick)
- macOS:
tailwindcss-rails(engine UI theming) andmaquina_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.
0sends 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 thelexxygem 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.