Back to Tutorials
Tutorial10/22/2025

Mailstify #1 - Let's Build A Zero-Dependency Mailchimp Clone in with Ruby on Rails 8

Mailstify #1 - Let's Build A Zero-Dependency Mailchimp Clone in with Ruby on Rails 8

Welcome to the first installment of our tutorial series, Mailstify: Building a Simple Mailchimp Clone with Native with Ruby on Rails 8.

Mailstify Table of Contents

Introduction

What is Mailchimp? Mailchimp is a widely used email marketing platform that allows users to manage subscriber lists (Audiences), design rich email content (Campaigns), and dispatch those campaigns in bulk. Our simple clone will focus on implementing these core features: Audience Management (Lists & Subscribers) and Asynchronous Campaign Dispatch.

The Rails 8 Native Principle: Crucially, we are adhering to a zero-external-dependency principle for core functionality. This project leverages Rails 8's powerful built-in toolset to demonstrate modern Rails development:

  • Action Text: For creating rich-text email content within Campaigns.
  • Action Mailer: For handling the email sending logic.
  • Active Job / Solid Queue: For offloading the bulk email dispatch to the background, ensuring fast, responsive user interactions without using external services like Redis or Sidekiq.
  • Hotwire (Turbo & Stimulus): For modern, dynamic front-end updates and list filtering without writing custom JavaScript.

Let's begin by setting up the project and defining the data structure.

1. Project Setup and Initialization

We start by generating a new Rails application. We will use SQLite for simplicity and development speed, and add the --skip-test flag to focus solely on the implementation.

# Generate the new Rails project
rails new mailchimp_clone --skip-test
cd mailchimp_clone

# Run bin/setup to prepare the database
bin/setup

Install Action Text & Solid Queue Dependencies

Since we are using Action Text and Solid Queue (the native job backend) which require their own tables, we need to run their setup commands now.

# 1. Install default dependencies
bundle install

# 2. Install Action Text (for rich content editor)
bin/rails action_text:install

# 3. Install Solid Queue (for asynchronous background jobs)
# This creates the necessary configuration and database schema files.
bin/rails solid_queue:install

# 4. Run migration
rails db:migrate

Confirm the server starts correctly:

rails s
# => Access <http://localhost:3000> to verify the default Rails page.

2. Core Model Design and Generation

Our system requires three interconnected models: ListSubscriber, and Campaign. We will use the built-in scaffold generator where appropriate to rapidly build CRUD interfaces.

A. Model: List (The Audience Container)

The List model organizes our subscribers.

# Use scaffold for quick CRUD interface
bin/rails generate scaffold List name:string

# Model: List
# - List has many Subscribers
# - List has many Campaigns

B. Model: Subscriber (The Contact)

The Subscriber is the individual recipient. Each subscriber belongs to a specific List.

# Use scaffold, defining the foreign key (list:references)
bin/rails generate scaffold Subscriber email:string name:string list:references

# Model: Subscriber
# - Subscriber belongs to a List

C. Model: Campaign (The Email Content)

The Campaign holds the subject and content. Since the content is rich text, we only define the name and subject fields, and the list:references foreign key.

# Use model generator, as Action Text will handle the content field (body)
bin/rails generate model Campaign name:string subject:string list:references

D. Running Migrations

Execute the migrations to create all necessary tables, including the ones for Action Text and Solid Queue:

bin/rails db:migrate

3. Defining Associations and Validations

The generated models must be updated to enforce data integrity and define the correct object relationships. This is where we ensure the required associations are in place for the console demonstration.

A. app/models/list.rb

The List model must manage its dependencies, defining the associations and ensuring that if a list is deleted, its associated subscribers and campaigns are also destroyed (dependent: :destroy).

# app/models/list.rb
class List < ApplicationRecord
  has_many :subscribers, dependent: :destroy
  has_many :campaigns, dependent: :destroy
end

B. app/models/subscriber.rb

We must ensure the email format is valid and enforce the critical business rule: an email can only be subscribed once to the same list.

# app/models/subscriber.rb
class Subscriber < ApplicationRecord
  belongs_to :list

  # Validation (must be added manually after scaffold)
  validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
  # Critical business rule: scoped uniqueness
  validates :email, uniqueness: { scope: :list_id, message: "is already subscribed to this list" }
end

C. app/models/campaign.rb

The Campaign must be linked to a target list and be equipped with the Action Text rich editor for the email body.

# app/models/campaign.rb
class Campaign < ApplicationRecord
  belongs_to :list

  # This line connects the Campaign to the ActionText table
  has_rich_text :body

  # Basic validations (must be added manually)
  validates :name, :subject, presence: true
end

D. Configuring Solid Queue

To use Solid Queue, we must tell Active Job to use it as the adapter. Open config/environments/development.rb and add the following line inside the Rails.application.configure do block:

# config/environments/development.rb
config.active_job.queue_adapter = :solid_queue

4. Console Demonstration: Testing the Models

Before moving on, let's use the Rails console to immediately verify that our models, associations, and validations are working as expected.

bin/rails c

A. Creating and Associating Data

We will create one list, one subscriber, and one campaign.

# 1. Create the main List
main_list = List.create(name: 'Marketing Newsletter')
puts "List ID: #{main_list.id}"
# => List ID: 1

# 2. Create the first Subscriber (Success)
sub_alpha = main_list.subscribers.create(name: "Alex", email: "alex@example.com")
puts "Subscriber Alpha valid? #{sub_alpha.valid?}"
# => Subscriber Alpha valid? true

# 3. Create the Campaign for the List
campaign_q3 = main_list.campaigns.create(name: "Q3 Product Launch", 
subject: "Check out our new features!", 
body: "Welcome to our Q3 product launch!")
puts "Campaign ID: #{campaign_q3.id}"
# => Campaign ID: 1

B. Verifying Associations

Use the objects to navigate the relationships we defined:

# Verify List -> Subscribers association
puts "Subscribers in List: #{main_list.subscribers.count}"
# => Subscribers in List: 1

# Verify Subscriber -> List association
puts "Subscriber Alpha's List Name: #{sub_alpha.list.name}"
# => Subscriber Alpha's List Name: Marketing Newsletter

# Verify List -> Campaign association
puts "List Campaigns: #{main_list.campaigns.first.name}"
# => List Campaigns: Q3 Product Launch

C. Testing Uniqueness Validation

Now, try to add the exact same subscriber to the same list. This must fail due to the uniqueness: { scope: :list_id } validation.

# Attempt to create a duplicate subscriber on the same list
sub_duplicate = main_list.subscribers.create(name: "Alex 2", email: "alex@example.com")
puts "Subscriber Duplicate valid? #{sub_duplicate.valid?}"
# => Subscriber Duplicate valid? false

# Check the specific error message
puts "Validation Error: #{sub_duplicate.errors.full_messages.join(', ')}"
# => Validation Error: Email is already subscribed to this list

The console confirms that all three models, their associations, and the critical scoped uniqueness validation are implemented correctly.


5. Setting the Root Route

To finalize the foundation, we define the application's starting point.

# config/routes.rb
Rails.application.routes.draw do
  resources :campaigns
  resources :subscribers
  resources :lists
  # Sets the root path
  root "lists#index"
end

Restart the server (rails s). Visiting http://localhost:3000 will now display the basic Lists management page, ready for data entry.

You can find all the source code for this tutorial here: Mailstify .


Conclusion of Part 1

We have established a solid data structure and verified its integrity using the Rails console. This foundation adheres strictly to the Rails Native principle. In Part 2, we will focus on Audience Management and Hotwire integration, enhancing the Subscribers interface to allow for dynamic addition and viewing of contacts within a specific list, mimicking the list view functionality of Mailchimp.

Comments

Loading comments...

Level Up Your Dev Skills & Income 💰💻

Learn how to sharpen your programming skills, monetize your expertise, and build a future-proof career — through freelancing, SaaS, digital products, or high-paying jobs.

Join 3,000+ developers learning how to earn more, improve their skills, and future-proof their careers.