Build on Rails

Build a newsletter system on Rails

Run your list from your Rails app: subscribers, segments, and campaigns you compose, schedule, and send with Action Mailer, with bounces and unsubscribes handled on your own data.

250+ paying companies · 2.7M downloads of the avo gem

01

01 · the hand-built version

What building a newsletter system by hand actually costs

Sending email is the easy part. The system around it, who is subscribed, who bounced, and what went out to whom, is what turns a mailer into a product you have to maintain.

  • 01

    The list is never just an email column

    Real lists have segments, double opt-in state, and per-subscriber consent. Once you are filtering active subscribers in the product-updates segment who have not bounced, you have built query logic you now own and test.

  • 02

    Bounces and unsubscribes are law, not polish

    You have to honor an unsubscribe immediately and stop mailing addresses that hard-bounce. Wiring provider webhooks to subscriber state, and proving you did, is unglamorous work with real consequences if you skip it.

  • 03

    A send is a long-running job you need to watch

    Blasting ten thousand emails means batching, throttling, retries, and a progress readout. Did the Tuesday issue actually finish sending should be answerable at a glance, not by tailing logs.

  • 04

    Composing an issue needs a real editor and a preview

    Writing a campaign means rich text, a subject, a segment, and a send time, plus a way to see it before it goes. That is a small CMS, and it is yours to build and keep working.

02

02 · the Avo version

Your models are already the hard part. Avo is the rest.

Avo turns the models behind a newsletter, Subscriber, Segment, and Campaign, into a place you actually run the list. You compose issues, pick a segment, schedule a send, and trigger delivery through your existing Action Mailer setup, all on your own tables with no separate email platform to sync to.

Subscribers and segments you can actually work

A Subscriber resource with a status badge and a subscribed_at date makes list state legible, and filters let you pull active in this segment, no recent bounce in one click instead of a console query.

Compose a campaign like a mini CMS

A Campaign resource with a subject, a rich-text body, a segment belongs_to, and a scheduled_at field gives you a real authoring surface. Preview it on the Show view before anything goes out.

Send through Action Mailer with one action

Model Send campaign and Send test to me as Avo actions that enqueue your existing mailer and background job. The button does the send, your delivery code stays exactly where it is.

Bounces and unsubscribes reflected on the record

Your provider webhook updates subscriber status, and Avo shows it as a badge and lets you filter the bounced and unsubscribed out. Honoring an opt-out becomes visible state, not a buried callback.

Watch a send finish

Expose delivery counts and status on the campaign so is the Tuesday issue done is answered on the page. Progress and failures are data on the record, not something you tail logs for.

A Campaign resource with a segment, schedule, and send actions:

class Avo::Resources::Campaign < Avo::BaseResource
  self.title = :subject
  self.includes = [:segment]

  def fields
    field :id, as: :id
    field :subject, as: :text, required: true
    field :status, as: :badge, options: {
      neutral: "draft",
      info: "scheduled",
      warning: "sending",
      success: "sent"
    }
    field :segment, as: :belongs_to
    field :body, as: :trix
    field :scheduled_at, as: :date_time
    field :recipients_count, as: :number, only_on: :show
    field :sent_count, as: :number, only_on: [:index, :show]
  end

  def actions
    action Avo::Actions::SendCampaign
    action Avo::Actions::SendTestEmail
  end
end
03

03 · the timeline

From first resource to the team running on it

Hour one

Generate resources for your Subscriber, Segment, and Campaign models and you are looking at your real list, filterable by segment and status, with campaigns you can open and read.

Day one

Put it behind your authentication, wire Send campaign to your existing Action Mailer job, and send a real issue to a real segment from the campaign's page.

End of the week

Provider webhooks keep bounce and unsubscribe status current, sends run as background jobs with a progress readout, and the throwaway rake task you were sending from is gone.

Prefer to prompt?

Hand this to Claude Code, Cursor, or whatever agent you build with, and review what it ships like any other pull request.

Build a newsletter system as a brand-new Ruby on Rails application, with Avo 4 (https://avohq.io, docs at https://docs.avohq.io/4.0/) as the management UI. There is no existing app or schema to work from, so this scaffolds the whole thing. Delivery runs on Action Mailer inside this new app.

1. Run rails new with sensible defaults to create the app. Generate authentication with Rails' built-in authentication generator so there is a real login before anything else exists. Add the avo gem, bundle, run bin/rails generate avo:install, and mount Avo at /avo so the generated authentication guards it.
2. Design the data model from first principles rather than copying a fixed schema. Think through what running a list genuinely requires: who the people on the list are and the consent state that governs whether you may mail them, how those people get grouped so a send can target a slice of them, and the issues that get composed, scheduled, and dispatched. Decide which states belong in an enum on a record (a subscriber being subscribed, unsubscribed, or bounced; a campaign moving through draft, scheduled, sending, and sent) and which deserve their own table, and work out the associations between them. A campaign needs somewhere to record how many people it targeted and how many actually received it. Write down the entities, their states, and their relationships, then generate the migrations to match.
3. Generate an Avo resource for each model with bin/rails generate avo:resource (https://docs.avohq.io/4.0/resources.html). On the campaign resource, point self.title at the subject line, list the grouping association in self.includes so the index does not run N+1 queries, and match fields to the columns you created: a trix field for the body, a badge over the lifecycle status, a belongs_to to the group it targets, a date_time for the scheduled send, and number fields on the Show view for the targeted and delivered counts. On the subscriber resource, set self.search over name and email and put a badge on the consent status so the list reads at a glance.
4. Because this app is new, the delivery machinery does not exist yet, so build it: an Action Mailer mailer for a campaign, and a background job that batches and throttles the send, updates the delivered count as it goes, and honors suppressed addresses. Wire the verbs of running a list as Avo Actions (https://docs.avohq.io/4.0/actions.html) that call into that code: one that dispatches a campaign to its group, and one that sends a test copy to the signed-in user before a real send.
5. Add filters and scopes so the subscriber list can be narrowed to a group with no recent bounce, and the campaign list to what is still in draft versus already sent. Build an Avo dashboard (https://docs.avohq.io/4.0/dashboards.html) with metric cards for list size, subscribers gained recently, and campaigns sent, so the health of the list is visible on landing. Add an endpoint that receives your email provider's webhooks and flips a subscriber to unsubscribed or bounced, and surface that as filterable state on the resource.
6. Add Pundit authorization (https://docs.avohq.io/4.0/authorization.html) with two roles: an admin who can do everything, and an editor who can compose and schedule campaigns but cannot delete subscribers or export the list.
7. Seed the database with believable sample data: a few hundred subscribers spread across a couple of groups and consent states, and a handful of campaigns in different lifecycle stages, so every resource, filter, and dashboard card has something real to render.
8. Boot the app, sign in, open /avo, compose a short campaign, and send a test copy to yourself to confirm the mailer, the job, and the delivered count all move.

04 · start today

Stand up a newsletter system this week

Avo is per app with unlimited users, so the whole team works from it without the bill scaling with headcount. When you hit an edge Avo doesn't cover, you drop to plain Rails instead of filing a feature request.

More things teams run from their Rails app

Frequently asked questions

Can I build a newsletter system with Ruby on Rails?
Yes. Subscribers, segments, and campaigns are ordinary Rails models, and delivery is Action Mailer plus a background job you likely already have. Avo gives you the management surface on top, composing issues, picking segments, and triggering sends, without you building an admin from scratch. It runs inside your Rails app, so your list never leaves your database.
Does it send email itself or use my provider?
It uses yours. Avo does not replace Action Mailer or your delivery provider. A Send campaign action enqueues the same mailer and background job you would write anyway, so you keep full control of delivery and there is no separate email platform to sync your list to.
How are unsubscribes and bounces handled?
As subscriber state on your own records. Your provider's webhook updates a subscriber's status when they unsubscribe or hard-bounce, and Avo shows that as a badge you can filter on, so honoring an opt-out and suppressing bad addresses is visible, queryable state rather than a buried callback.
Can I schedule a campaign to send later?
Yes. A scheduled_at field on the campaign plus your existing scheduler, a cron job or a scheduled background job, sends the issue at the chosen time. Avo is where you set the schedule and watch the status move from scheduled to sending to sent.
How do I segment subscribers?
A campaign belongs_to a segment, and the subscriber list is filtered by that segment plus status. You reuse the same Active Record scopes your app already trusts, so active subscribers in the product-updates segment, no recent bounce is a filter, not a new query you have to write and test twice.
What does it cost to run?
Avo 4 is priced per app with unlimited users, so everyone who edits or sends campaigns is included at no extra cost. Over 250 paying companies run on Avo, and the avo gem has 2.7M downloads.