Open-source project
honeybadger-io/heya avatar
honeybadger-io/heya

heya adds scheduled email sequences to a Rails app without taking over your user table

Heya 👋 is a campaign mailer for Rails. Think of it like ActionMailer, but for timed email sequences. It can also perform other actions like sending a text message.

766 stars35 forksRubyNOASSERTION

At a glance

What is it?
Heya is a campaign mailer for Rails, described as ActionMailer for timed email sequences, where a campaign is a list of named steps and a scheduler task moves users through them. It reads your existing User model and never writes to it, and it is built for PostgreSQL only.
Who is it for?
heya fits a Rails product that needs an onboarding or drip sequence without pulling in a marketing platform, and it fits teams who want the sequence to live in the same repository, the same review process and the same test suite as the rest of the app.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 12 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

ActionMailer for sequences, with a user table it never touches

Heya is a campaign mailer for Rails, and the README gives the analogy that explains the whole design: think of it like ActionMailer, but for timed email sequences. It can also perform other actions like sending a text message.

That framing is accurate in a specific way. A campaign is a class in your app, with a list of steps, and each step is a message with a subject and a body. What ActionMailer does not have is the passage of time between messages, and that is what Heya adds.

The part of the design that most distinguishes it from a hosted drip marketing tool is the user data story. The README states that Heya does not store a copy of your user data; instead it reads from your existing User model and never writes to it. If your user model is called something else, you change the user_type configuration option in config/initializers/heya.rb.

So the campaign state lives in Heya's own tables, and the identity of the recipient is resolved at send time by reading your model. The practical benefit is that you are not synchronising a mailing list with your database, and the practical cost is that Heya has to be able to find your user records at the moment it sends.

The project is built by Honeybadger, the error monitoring company, and is distributed as the heya gem with a homepage at heya.email.

Install the gem, run one generator, migrate

The README's getting started is three steps: install the gem, create a campaign, run the scheduler. The install step is a Gemfile line pointing at the GitHub repository:

ruby
gem "heya", github: "honeybadger-io/heya"

Note that the documented source is the GitHub repository rather than a released gem version, which is worth remembering when you decide how to pin the dependency. The README also notes that the gem has a version badge on RubyGems, and the repository carries a CHANGELOG.md and an UPGRADING.md for the times when it does move.

After adding the line, the install commands are:

bash
bundle install
rails generate heya:install
rails db:migrate

The generator does four things: it copies Heya's migration files into db/migrate, it copies the default initializer to config/initializers/heya.rb, it creates app/campaigns/application_campaign.rb, and it runs the local migrations.

So the whole setup is one generator and one migrate. The campaigns themselves live in app/campaigns, in the same place Rails puts mailers in app/mailers, and the views land under app/views/heya/campaign_mailer.

The one database constraint appears in the prerequisites section: Heya was built to work with PostgreSQL, and pull requests to support other databases are welcome. That is a real limit for an application on MySQL or SQLite.

A scheduler task and a queue name, nothing else

Sending happens when you run the scheduler task:

bash
rails heya:scheduler

The README describes it as a task to run periodically, and that is the entire runtime story. Heya is not a resident process that wakes itself up; something else in your infrastructure has to call that task on a schedule, exactly as you would run a cron job or a periodic job in a Rails app.

Once a message is due, delivery is handed to ActiveJob. The README says Heya uses ActiveJob to send emails in the background, and warns that you must make sure your ActiveJob backend is configured to process the heya queue. If you run Sidekiq, the example given is:

sh
bundle exec sidekiq -q default -q heya

The queue name is configurable per campaign, because the default can be overridden in the base campaign class:

ruby
# app/campaigns/application_campaign.rb
class ApplicationCampaign < Heya::Campaigns::Base
  default queue: "custom"
end

Two operational consequences follow. First, if your worker does not consume the heya queue, messages queue up silently and nothing is sent, so the queue has to be part of your worker configuration from the first deploy. Second, since the scheduler is an external trigger, the resolution of your send schedule determines the granularity of your timing.

The default wait between steps is two days, calculated from the time the user completed the previous step, or from the time they entered the campaign for the first step. That delay is measured per user, not per campaign, so a user who joins late does not receive the whole sequence compressed into a short window.

A step name is an identifier, so renaming one resends it

The step method defines a new step in the sequence, and Heya completes each step in the order it appears once a user is added to the campaign. A campaign generated with three step names looks like this:

ruby
# app/campaigns/application_campaign.rb
class ApplicationCampaign < Heya::Campaigns::Base
  default from: "[email protected]"
end

# app/campaigns/onboarding_campaign.rb
class OnboardingCampaign < ApplicationCampaign
  step :first,
    subject: "First subject"

  step :second,
    subject: "Second subject"

  step :third,
    subject: "Third subject"
end

The README is emphatic about the one rule that bites: the most important part of each step is its name, which must be unique to the campaign. The name is how Heya tracks which user has received which message, so changing it after the campaign is active means Heya assumes it is a new message.

That is a genuine operational hazard rather than a theoretical one. If a campaign is live and someone renames a step to tidy up the code, everyone still in the sequence receives that message again, because from the database's point of view that message has never been sent to them. Renaming a subject is safe. Renaming a step is not.

The generator also creates the view directory and an ActionMailer preview, so the campaign is browsable in a browser before any of it is sent.

Priority decides which sequence a user receives first

When one user is in several campaigns at once, Heya has to choose an order, and that choice is configurable. The initializer holds the options:

ruby
Heya.configure do |config|
  # The name of the model you want to use with Heya.
  config.user_type = "User"

  # The default options to use when processing campaign steps.
  config.campaigns.default_options = {from: "[email protected]"}

  # Campaign priority. When a user is added to multiple campaigns, they are
  # sent in this order. Campaigns are sent in the order that the users were
  # added if no priority is configured.
  config.campaigns.priority = [
    "FirstCampaign",
    "SecondCampaign",
    "ThirdCampaign"
  ]
end

The priority list is a list of campaign class names, and it means the first campaign in the list is served first. If no priority is configured, campaigns are served in the order the users were added to them.

The default_options setting covers the options applied when a campaign step is processed, with the from address as the example. The user_type setting is the one that decides where Heya looks for recipients, and it defaults to User, so an application with a different model has to change it before the first send rather than after.

Users are added to a campaign from your own code, which is the part that ties the sequence to your application's events:

ruby
OnboardingCampaign.add(user)

and the README shows the typical hook that adds a user at sign up time:

ruby
after_create_commit do
  OnboardingCampaign.add(self)
end

Using after_create_commit rather than after_save means the user row is committed before Heya tries to read it, which matters given that Heya reads your model and never writes to it.

MailCatcher, Maildown and previews for the development loop

Heya sits on top of ActionMailer, so the standard Rails tooling for email applies, and the README collects the three that matter.

MailCatcher is the way to see messages in development. The suggested configuration switches the delivery method to SMTP and points it at the local MailCatcher port:

ruby
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {host: "localhost", port: 1025}

Maildown lets you write the emails in Markdown, and it composes with the Heya generator:

sh
$ bundle add maildown
$ rails generate heya:campaign Onboarding welcome
      create  app/campaigns/application_campaign.rb
      create  app/campaigns/onboarding_campaign.rb
      create  app/views/heya/campaign_mailer/onboarding_campaign/welcome.md.erb

The README points out the detail that makes this worth doing: only one template is generated, because Maildown builds the HTML and text variants from the markdown file.

Third, previews. The Heya campaign generator creates an ActionMailer preview at (test|spec)/mailers/previews/*_campaign_preview.rb, which you open at localhost:3000/rails/mailers/. If you did not use the generator you can still write your own:

ruby
# test/mailers/previews/onboarding_campaign_preview.rb
class OnboardingCampaignPreview < ActionMailer::Preview
  def welcome
    OnboardingCampaign.welcome(user)
  end

  private

  def user
    User.where(id: params[:user_id]).first || User.first
  end
end

For a project that sends a lot of sequence mail, having each message previewable in a browser without sending it is the difference between iterating quickly and not.

PostgreSQL only, a custom licence, and no tagged releases

Three constraints are worth putting next to the feature set.

The first is the database. Heya was built to work with PostgreSQL, and the README says pull requests are welcome to support more databases, which is an invitation rather than a promise. If your Rails app runs on MySQL or SQLite, you are reading someone else's patch at best.

The second is the licence. The project is published under a custom licence that GitHub cannot classify, and the repository root holds the LICENSE file. Nothing here is legal advice, but the point stands: a custom licence may carry terms that MIT or Apache would not, so read it before you build a business process on top of the gem.

The third is the release record. The repository has no GitHub releases, and the last push was on 2026-10-12. The gem is installed from the GitHub repository in the documented setup, which is consistent with that, but it means your dependency on a moving target is worth pinning to a specific commit if stability matters to you.

The repository layout suggests a more mature process than the release record shows. There is a CHANGELOG.md, an UPGRADING.md, an Appraisals file and a gemfiles/ directory, which is the standard way to test a gem against several dependency sets, a Rakefile, a .standard.yml for the standard Ruby linter, and a test/ directory with a workflow badge for the test run.

Editorial conclusion

heya fits a Rails product that needs an onboarding or drip sequence without pulling in a marketing platform, and it fits teams who want the sequence to live in the same repository, the same review process and the same test suite as the rest of the app. It is the wrong choice if you need something other than PostgreSQL, since support for other databases is an open invitation rather than a feature, and it is the wrong choice if you need per-user branching, because the documented model is a linear list of steps. Verify first that your licence terms suit you, since the project ships a custom licence rather than a standard one, that your ActiveJob backend consumes the heya queue, and that you understand the step naming rule before a campaign goes live, because renaming a step after users have started it makes Heya treat it as a new message.

Frequently asked questions

What is the heya gem for?

Heya is a campaign mailer for Rails, described as ActionMailer but for timed email sequences. Campaigns are lists of named steps, users are added to a campaign from your code, and a scheduler task plus an ActiveJob queue deliver the messages.

How do I install and set up heya in a Rails app?

Add gem "heya", github: "honeybadger-io/heya" to the Gemfile, then run bundle install, rails generate heya:install and rails db:migrate. The generator copies the migrations, the initializer, and an application campaign class.

How are heya emails actually sent?

You run the scheduler task periodically with rails heya:scheduler, and Heya hands delivery to ActiveJob on the heya queue. Your worker has to consume that queue, for example bundle exec sidekiq -q default -q heya.

Can heya use a user model other than User?

Yes. Set config.user_type in config/initializers/heya.rb to the name of your model, for example config.user_type = "MyUser". Heya reads from that model and never writes to it, and it does not keep its own copy of your user data.

What happens if I rename a heya campaign step?

The step name is how Heya tracks which user has received which message, so it must be unique to the campaign. The README warns that changing a name after a campaign is active makes Heya assume it is a new message, so the message is sent again.

Official sources

  1. honeybadger-io/heya on GitHub
  2. Issues
  3. Project website
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/honeybadger-io-heya.svg)](https://hysenlabs.com/projects/honeybadger-io-heya)

Was this useful?