AshDispatch

Hex.pmHexdocsLicense: MIT

⚠️ Experimental — v0.5, public launch. AshDispatch was just open-sourced and is very early / experimental. It already powers notifications in a production application (this is the extracted, generalized engine), but the public API is still settling and may change between minor versions. Pin an exact version, expect rough edges, and please open issues — feedback now directly shapes 1.0.


AshDispatch is an event-driven notification and messaging system for Ash Framework. It provides a declarative DSL for defining events in your resources and automatically dispatching them across multiple transports (email, in-app notifications, broadcast/PubSub, Discord, Slack, webhooks, etc.).

Why AshDispatch?

Declarative Event Definitions

Define events directly in your resources using familiar Ash DSL patterns:

defmodule MyApp.Orders.ProductOrder do
use Ash.Resource,
extensions: [AshDispatch.Resource]
actions do
create :create_from_cart do
accept [:user_id]
# Your action logic...
end
end
dispatch do
event :created,
trigger_on: :create_from_cart,
priority: :standard,
channels: [
[transport: :in_app, audience: :user],
[transport: :email, audience: :user, delay: 300]
],
content: [
subject: "Order #{{order_number}} created",
notification_title: "Your order was created",
notification_message: "Order #{{order_number}} is being processed"
],
metadata: [
notification_type: :success
]
end
end

Key Features

Tutorials

Topics

Reference

Architecture Overview

graph TB
A[Resource Action] -->|triggers| B[Event]
B -->|creates| C[DeliveryReceipt]
C -->|dispatches to| D{Transport}
D -->|in_app| E[Notification]
D -->|email| F[Oban Job]
D -->|discord| G[Webhook]
F -->|sends| H[Email Service]
E -->|updates| I[User UI]
G -->|posts| J[Discord Channel]

Installation

mix igniter.install ash_dispatch

This creates all required resources, domains, and configuration automatically. Then run migrations:

mix ash.codegen add_ash_dispatch
mix ash.migrate

Manual Installation

def deps do
[
{:ash_dispatch, "~> 0.5.0"}
]
end

See Getting Started for complete manual setup instructions.

Production Configuration (Required for Releases)

Important: For production releases, you must enable template compilation. Without this, email templates won't load in production because lib/ source files aren't included in releases.

# config/prod.exs - REQUIRED for releases
config :ash_dispatch,
compile_templates: true

This copies templates to priv/ash_dispatch/templates/ during compilation, ensuring they're available at runtime. See Code Generation for details.

Quick Example

# 1. Add extension to resource
defmodule MyApp.Tickets.Ticket do
use Ash.Resource,
extensions: [AshDispatch.Resource]
# 2. Define events
dispatch do
# Inline-complete event (no module needed!)
# Works out of the box with just DSL configuration
event :created,
trigger_on: :create,
channels: [
[transport: :in_app, audience: :user]
],
content: [
notification_title: "Ticket Created",
notification_message: "{{user_name}} created ticket #{{id}}"
],
metadata: [
notification_type: :success
]
# Event with email requires templates
# Run `mix ash.codegen` to generate template stubs
event :assigned,
trigger_on: :assign,
channels: [
[transport: :in_app, audience: :user],
[transport: :email, audience: :user]
],
content: [
subject: "Ticket #{{id}} assigned to you",
notification_title: "Ticket Assigned"
]
# Complex event with custom module (your override)
event :escalated,
trigger_on: :escalate,
module: MyApp.Events.Tickets.Escalated
end
end
# 3. That's it! Events dispatch automatically when actions run
Ticket
|> Ash.Changeset.for_create(:create, %{title: "Bug report"})
|> Ash.create!()
# -> Automatically dispatches :created event
# -> Creates in-app notification for user (no templates needed!)

Hybrid Architecture

AshDispatch uses a hybrid approach where DSL configuration takes precedence, and generated modules provide fallbacks:

  1. Define events in DSL - Configure channels, content, and metadata directly in your resource

  2. Run mix ash.codegen - Generates event modules with templates for each event (unless you provide module: override)

  3. Hybrid dispatch - At runtime, DSL content is used first; module callbacks fill in gaps (templates, recipients, etc.)

Override with custom modules: Set module: to provide your own implementation for complex events (custom recipients, conditional sending, etc.)

Real-Time Counter Broadcasting

AshDispatch also provides automatic real-time counter updates with zero boilerplate:

# 1. Define counters in your resource
defmodule MyApp.Orders.ProductOrder do
use Ash.Resource,
extensions: [AshDispatch.Resource]
counters do
# User sees their own pending orders
counter :pending_orders,
trigger_on: [:create, :complete, :cancel],
counter_name: :pending_orders,
query_filter: [status: :pending],
audience: :user,
invalidates: ["orders"]
# Admins see ALL pending orders
counter :admin_pending_orders,
trigger_on: [:create, :complete, :cancel],
counter_name: :admin_pending_orders,
query_filter: [status: :pending],
audience: :admin,
invalidates: ["orders", "analytics"]
end
end
# 2. Configure broadcasting (one line!)
# config/config.exs
config :ash_dispatch,
counter_broadcast_fn: {MyAppWeb.UserChannel, :broadcast_counter}
# 3. Use helper in Phoenix Channel (one line!)
defmodule MyAppWeb.UserChannel do
alias AshDispatch.Helpers.ChannelState
def handle_info(:after_join, socket) do
# Loads ALL counters automatically - no manual queries!
initial_state = ChannelState.build(socket.assigns.user_id)
# => %{"counters" => %{"pending_orders" => 5}, "notifications" => [...]}
push(socket, "initial_state", initial_state)
{:noreply, socket}
end
end
# 4. That's it! Counters update in real-time automatically
Order.create!(%{status: :pending})
# -> Automatically broadcasts counter update to Phoenix Channel
# -> Frontend receives "counter_updated" event with new value

Zero configuration, automatic discovery, real-time updates!

See Counter Broadcasting and Phoenix Integration for complete guides.

Internal Architecture

AshDispatch uses centralized resolver modules to ensure consistent behavior across all dispatch pathways:

ChannelResolver

AshDispatch.ChannelResolver handles all channel resolution with consistent priority logic:

# Used internally by Dispatcher, ManualTrigger, preview generators, etc.
channels = AshDispatch.ChannelResolver.resolve(event_id, event_module, context,
dsl_channels: event_config.channels
)

EventResolver

AshDispatch.EventResolver centralizes event module discovery and safe callback execution:

# Find an event module
{:ok, module} = AshDispatch.EventResolver.find_module("orders.created")
# Safe callback execution with defaults
subject = AshDispatch.EventResolver.subject(module, context, channel, default: "No subject")

Naming

AshDispatch.Naming handles consistent filename and label generation for multi-audience templates:

# Generate audience-specific filenames
AshDispatch.Naming.filename("email", :admin, "summary", "html")
# => "email.admin.summary.html"
# Generate display labels
AshDispatch.Naming.label(:email, :admin, "summary")
# => "email (admin, summary)"

Design Principles

1. Resource-Centric

Events are defined in resources, just like actions, attributes, and relationships.

2. Progressive Complexity

Start with simple inline events. Upgrade to callback modules when you need custom logic.

3. Receipt-First Pattern

All deliveries create a receipt record before dispatch, enabling full audit trails and reliable retries.

4. Fail-Safe Defaults

User preferences, rate limiting, and delivery policies protect users from notification fatigue.

5. Framework Integration

Deep integration with Ash actions, Oban jobs, and the Ash ecosystem.

6. Centralized Logic

Channel resolution, event discovery, and naming all use centralized modules to ensure consistency and maintainability.

Project Status

v0.5 — public launch, experimental. AshDispatch is published on Hex and open for use, but it's early days:

It's extracted from a production application, so the core paths are proven — but treat the public package as experimental and pin an exact version.

Contributing

AshDispatch is open source and contributions are welcome. Issues and PRs go to github.com/Vulcora/ash_dispatch. Since the API is still evolving, please open an issue to discuss larger changes before submitting a PR.

License

MIT License - see LICENSE file for details.

Acknowledgments

Built on the excellent Ash Framework by Zach Daniel and the Ash community.

Inspired by patterns from AshStateMachine, AshAuthentication, and years of building notification systems.