AshDispatch
Status: ๐ง Active Development - Extracting proven notification engine from Magasin into reusable Ash extension
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, 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,
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
endKey Features
- ๐ฏ Automatic Dispatch - Events are automatically triggered by resource actions
- ๐ฌ Multi-Transport - Email, in-app, Discord, Slack, SMS, webhooks out of the box
- โฐ Delayed Delivery - Schedule notifications for later delivery
- ๐ค User Preferences - Respect user notification preferences automatically
- ๐ Delivery Tracking - Full audit trail with delivery receipts
- ๐ Automatic Retries - Failed deliveries retry with exponential backoff
- ๐จ Template Interpolation -
{{variable}}syntax for dynamic content - ๐ Real-Time Counters - Declarative counter DSL with automatic Phoenix Channel broadcasting
- โก Zero-Config Helpers -
ChannelState,CounterLoader,NotificationLoaderfor Phoenix integration - ๐ Extensible - Add custom transports and event modules
- ๐งช Test-Friendly - Factory integration for testing templates
Tutorials
- Getting Started with AshDispatch - Basic event setup with inline DSL
- Manual Dispatch and Event Modules - Standalone events, manual triggers, and the two-path pattern
Topics
- What is AshDispatch?
- Phoenix Channel Integration - Zero-config helpers for real-time updates
- Counter Broadcasting - Declarative counter DSL with auto-discovery
- User Preferences
- Recipient Resolution
- Configuration
- App Integration
- Code Generation
- Oban Configuration
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
def deps do
[
{:ash_dispatch, "~> 0.1.0"}
]
endQuick Example
# 1. Add extension to resource
defmodule MyApp.Tickets.Ticket do
use Ash.Resource,
extensions: [AshDispatch.Resource]
# 2. Define events
dispatch do
# Simple inline event
event :created,
trigger_on: :create,
channels: [
[transport: :in_app, audience: :user],
[transport: :email, audience: :admin]
],
content: [
subject: "New ticket: {{title}}",
notification_title: "Ticket Created",
notification_message: "{{user_name}} created a new ticket"
]
# Complex event with callback module
event :status_changed,
trigger_on: [:resolve, :close, :reopen],
module: MyApp.Events.Tickets.StatusChanged
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
# -> Sends email to adminReal-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 valueZero configuration, automatic discovery, real-time updates!
See Counter Broadcasting and Phoenix Integration for complete guides.
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.
Development Status
Current: โ Resource extension complete and tested Next: ๐ง Runtime dispatcher and Domain extension
Contributing
This is currently being extracted from Magasin where it has been running in production. Once stabilized, it will be published as a standalone package.
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.