Framework
palkan/active_delivery avatar
palkan/active_delivery

Active Delivery: one delivery class in front of mailers, notifiers and push channels

Ruby framework for keeping all types of notifications (mailers, push notifications, whatever) in one place

664 stars17 forksRubyMIT

At a glance

What is it?
Active Delivery is a Ruby framework that gives a Rails app one entry point for notifications, with a Delivery class per resource that fans out to mailers, SMS notifiers, webhook notifiers and Action Cable lines. Since version 1.0 it bundles Abstract Notifier, and it can be used without Rails entirely.
Who is it for?
Active Delivery suits a Rails application whose notification logic has spread across mailers and ad hoc service objects and now needs one place to decide which channels a user gets, and the naming convention plus the line resolvers keep the routing out of business code.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 90 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

The problem is a fan-out decision that does not belong in business logic

The framework starts by naming the thing it fixes. Different notification channels, mail and push among them, need to be handled in one place, and from the business logic point of view you want to notify a user, which means you need a separate abstraction layer as the entry point to the different types of notification.

The before and after in the documentation make the change concrete:

ruby
# Before
def after_some_action
  MyMailer.with(user: user).some_action(resource).deliver_later if user.receive_emails?
  NotifyService.send_notification(user, "action") if whatever_else?
end

# After
def after_some_action
  MyDelivery.with(user: user).some_action(resource).deliver_later
end

The removed lines are where the channel decision lived. Whether the user gets email, whether a separate service sends a push, and the conditions on each, were all interleaved with the action that triggered them. The new line has none of that. The delivery class decides, and the call site stops caring.

The stated motivations are organising notification-related logic and better testability, with a Testing section referenced. The testability argument is the one that tends to matter more in practice, since a mailer that knows which channels to use can be asserted against once rather than through a set of conditionals in every action that happens to notify somebody.

The requirements are Ruby ~> 2.7 and Rails 6 or newer, with Rails optional. Most of the examples in the readme are Rails-specific, but the gem can be used without Rails or ActiveSupport, which matters if you are building notification handling in a plain Ruby service.

Lines are the channels, and mailers are the only default

The configuration model is a base delivery class in which you declare the lines, meaning the channels, that exist.

ruby
# In the base class, you configure delivery lines
class ApplicationDelivery < ActiveDelivery::Base
  self.abstract_class = true

  # Mailers are enabled by default, everything else must be declared explicitly

  # For example, you can use a notifier line (see below) with a custom resolver
  # (the argument is the delivery class)
  register_line :sms, ActiveDelivery::Lines::Notifier,
    resolver: -> { _1.name.gsub(/Delivery$/, "SMSNotifier").safe_constantize } #=> PostDelivery -> PostSMSNotifier

  # Or you can use a name pattern to resolve notifier classes for delivery classes
  # Available placeholders are:
  #  - delivery_class — full delivery class name
  #  - delivery_name — full delivery class name without the "Delivery" suffix
  register_line :webhook, ActiveDelivery::Lines::Notifier,
    resolver_pattern: "%{delivery_name}WebhookNotifier" #=> PostDelivery -> PostWebhookNotifier

  register_line :cable, ActionCableDeliveryLine
  # and more
end

The line at the top of that configuration carries the most important operational fact about the whole gem: mailers are enabled by default, everything else must be declared explicitly. That is a deliberate default. A new channel does not silently start sending messages because a gem was upgraded, which is the failure mode that makes notification layers frightening to maintain.

The two resolver styles cover the same ground in different ways. A resolver is a lambda that receives the delivery class and returns the notifier class, and the SMS example rewrites the class name by dropping the Delivery suffix and appending SMSNotifier. A resolver_pattern is a format string with two placeholders, delivery_class for the full class name and delivery_name for the full name without the Delivery suffix, and the webhook example produces PostDelivery to PostWebhookNotifier. The pattern form is the one most teams will use, since it needs no lambda per channel.

The ActionCable line is registered with a plain class, ActionCableDeliveryLine, because it is not a notifier and does not follow the naming convention.

Delivery classes stay empty, and the method call fans out

Once the lines are configured, a delivery class per resource is created, following Action Mailer conventions:

ruby
class PostsDelivery < ApplicationDelivery
end

In most cases you leave that class blank. The corresponding mailers and notifiers are inferred automatically from the naming convention.

There is no need to define notification methods explicitly. Invoking a method on a delivery class proxies it to the underlying line handlers:

ruby
PostsDelivery.published(user, post).deliver_later

# Under the hood it calls
PostsMailer.published(user, post).deliver_later
PostsSMSNotifier.published(user, post).notify_later

# You can also pass options supported by your async executor (such as ActiveJob)
PostsDelivery.published(user, post).deliver_later(wait_until: 1.day.from_now)

The alternative entry point is the notify method, which takes the notification name and the arguments:

ruby
PostsDelivery.notify(:published, user, post)

# Under the hood it calls
PostsMailer.published(user, post).deliver_later
PostsSMSNotifier.published(user, post).notify_later

When you do need logic in the notification itself, you define the method and return a delivery object so it can still be chained:

ruby
class PostsDelivery < ApplicationDelivery
  def published(user, post)
    # do something

    # return a delivery object (to chain #deliver_later, etc.)
    delivery(
      notification: :published,
      params: [user, post],
      # For kwargs, you options
      options: {},
      # Metadata that can be used by line handlers
      metadata: {}
    )
  end
end

The metadata key is the hook for passing information to line handlers, and options is where the async executor arguments go, which is what lets wait_until reach ActiveJob.

One thing to be aware of is that the empty class and the proxying behaviour mean a typo in a notification name does not fail loudly by default. The framework lets you close that hole with a configuration option, ActiveDelivery.deliver_actions_required = true, and then declare the allowed actions per class:

ruby
class PostDelivery < ApplicationDelivery
  delivers :published
end

ActiveDelivery.deliver_actions_required = true

PostDelivery.published(post) #=> ok
PostDelivery.whatever(post) #=> raises NoMethodError

With that option on, an undeclared action raises NoMethodError rather than being proxied to every channel. It is a good default for a codebase where a mistyped action would mean emailing every user.

Notifiers are pluggable, and Abstract Notifier came bundled in 1.0

The notifier side is a separate layer, and since version 1.0 Active Delivery is bundled with Abstract Notifier, with the documentation pointing to how to create custom notifiers.

A notifier is the adapter between a channel and your code. The SMS line in the base class resolves to PostsSMSNotifier, the webhook line to PostsWebhookNotifier, and those notifier classes are what actually talk to the provider. The Abstract Notifier part is what lets a notifier be defined and tested consistently, so a channel that does not exist yet can be added without inventing a new calling convention.

The pattern is consistent throughout. A delivery class named for a resource, PostDelivery, maps to a mailer named PostMailer and to notifiers named after the line, PostSMSNotifier and PostWebhookNotifier, with a notify_later method on the notifier side to match deliver_later on the mailer side. The gem does not hide that; it standardises the names so that a line added later works without touching existing code.

Because the notifier is an interface, the testability claim in the motivations follows. A test can assert that a delivery reached a notifier without a provider, a network call or a job queue. The README links a Testing section for the details, and points to a book chapter on designing a notifications layer in Ruby on Rails applications for the longer argument.

Where the classes live, and what the release history looks like

Two directory layouts are described, and picking one is a project decision rather than a library decision.

The first keeps things flat by type, with a deliveries/ directory holding application_delivery.rb, post_delivery.rb and user_delivery.rb, a mailers/ directory with application_mailer.rb, post_mailer.rb and user_mailer.rb, and a notifiers/ directory with application_notifier.rb, post_sms_notifier.rb and post_webhook_notifier.rb.

The second groups by resource, with a deliveries/ directory that contains application_delivery.rb alongside a post_delivery/ folder holding post_mailer.rb, post_sms_notifier.rb, post_webhook_notifier.rb and post_delivery.rb, and a user_delivery/ folder with the equivalent user files. The second layout keeps everything about a post in one place, which scales better as the number of channels per resource grows.

The repository is a conventional Ruby gem. The top level holds active_delivery.gemspec, a Gemfile, a Rakefile, a Makefile, CHANGELOG.md, RELEASING.md, a lefthook.yml for git hooks, rubocop configuration in .rubocop/ plus .rubocop.yml and .rubocop-md.yml for markdown, a .rspec file, a gemfiles/ directory for Appraisal style testing against dependency versions, lib/ for the code, spec/ for the tests and bin/ for executables. There is a .gem_release.yml, a .rbnextrc for the release automation, and a forspell.dict plus a .mdlrc for markdown linting and spell checking.

The Makefile shows the workflow, and it is a good indicator of the maturity of the tooling:

code
default: test

nextify:
	bundle exec rake nextify

test: nextify
	bundle exec rake
	CI=true bundle exec rake

The test target runs the suite twice, once normally and once with CI=true, and it depends on nextify first, which is a generation step for something the Rakefile produces.

Releases go through a release target that depends on test and lint, then checks git status, runs gem release with RELEASING_GEM=true, and pushes the branch and the tags. The published versions are 1.0.0 on 2023-08-29, 1.1.0 on 2023-12-01 and 1.2.0 on 2024-02-05, with the last push to the repository on 2026-07-25. The licence is MIT, in LICENSE.txt at the root, and the project is built by Evil Martians, with an introduction post linked from the readme.

Editorial conclusion

Active Delivery suits a Rails application whose notification logic has spread across mailers and ad hoc service objects and now needs one place to decide which channels a user gets, and the naming convention plus the line resolvers keep the routing out of business code. It does not suit a plain Ruby script that only sends email, where the wrapper adds a layer without adding channels, and it does not suit you if you need a notification type outside the resolver conventions without writing a custom notifier. Before you adopt it, check the delivery lines you actually need, since only mailers are enabled by default and every other line has to be declared explicitly, and read the .delivers option if you would rather have an unknown action raise than silently fan out.

Frequently asked questions

How do I install Active Delivery?

Add gem "active_delivery", "~> 1.0" to your Gemfile and run bundle install. The gem requires Ruby ~> 2.7, and Rails 6 or newer is optional, since the framework can be used without Rails or ActiveSupport.

Which notification channels are enabled by default in Active Delivery?

Mailers are enabled by default, and every other line must be declared explicitly with register_line. A line can use a resolver lambda or a resolver_pattern such as "%{delivery_name}WebhookNotifier".

How do I stop Active Delivery from proxying an unknown notification action?

Set ActiveDelivery.deliver_actions_required = true and declare the allowed actions in each delivery class with the delivers method. An action that is not declared then raises NoMethodError.

How do I send a notification on a schedule with Active Delivery?

Pass options supported by your async executor to deliver_later, for example PostsDelivery.published(user, post).deliver_later(wait_until: 1.day.from_now). The options are carried on the delivery object and reach ActiveJob.

Official sources

  1. Issues
  2. License: MIT
  3. palkan/active_delivery on GitHub
  4. README
  5. Releases
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/palkan-active-delivery.svg)](https://hysenlabs.com/projects/palkan-active-delivery)

Was this useful?