AshDispatch

License: MITDocumentation

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
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

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

Quick 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 admin

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.

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.