Configuration
View SourceAshDispatch is designed to be flexible and configurable. This guide covers all available configuration options and when to use them.
Required Configuration
Counter Broadcasting (for real-time counters)
Configure the function to call when broadcasting counter updates to Phoenix Channels:
# config/config.exs
config :ash_dispatch,
counter_broadcast_fn: {MyAppWeb.UserChannel, :broadcast_counter}Options:
# MFA tuple (recommended - easier to test)
counter_broadcast_fn: {MyAppWeb.UserChannel, :broadcast_counter}
# Or function capture
counter_broadcast_fn: &MyAppWeb.UserChannel.broadcast_counter/4Your broadcast function should accept:
user_id(string) - The user to broadcast tocounter_name(atom) - The counter name (e.g.,:pending_orders)value(integer) - The current countopts(keyword list) - Options including:metadatawithinvalidate_queries
Example implementation:
defmodule MyAppWeb.UserChannel do
def broadcast_counter(user_id, counter_name, value, opts \\ []) do
metadata = Keyword.get(opts, :metadata, %{})
MyAppWeb.Endpoint.broadcast("user:#{user_id}", "counter_updated", %{
counter: counter_name,
value: value,
metadata: metadata
})
end
endSee Phoenix Channel Integration for complete setup guide.
Email Backend (if using email transport)
Configure Swoosh for email delivery:
# config/config.exs
config :ash_dispatch,
email_backend: AshDispatch.EmailBackend.Swoosh,
swoosh_mailer: MyApp.Mailer
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.Resend,
api_key: System.get_env("RESEND_API_KEY")Oban (for async delivery)
Configure Oban for asynchronous email delivery:
config :my_app, Oban,
engine: Oban.Engines.Basic,
notifier: Oban.Notifiers.Postgres,
repo: MyApp.Repo,
queues: [
emails: 10 # AshDispatch uses :emails queue
]See Oban Configuration for complete setup.
Standalone Event Module Configuration
If you're using standalone event modules (with AshDispatch.Event behaviour) instead of the DSL-based approach, you need to configure these options:
Event Modules (Auto-Discovered)
Event modules are automatically discovered from your Ash domains - no manual configuration needed!
AshDispatch scans all resources with AshDispatch.Resource extension and finds:
- Events with explicit
module:option in DSL - Auto-generated event modules following the naming convention
# OLD - No longer needed!
# config :ash_dispatch, :event_modules, [...]
# NEW - Just configure your otp_app and domains
config :ash_dispatch, :otp_app, :my_app
config :my_app, :ash_domains, [
MyApp.Orders,
MyApp.Tickets
]The AshDispatch.EventRegistry module handles discovery:
EventRegistry.get_event_modules()- Returns[{event_id, module}, ...]EventRegistry.find_event("order.created")- Find specific eventEventRegistry.find_module("order.created")- Find event module
This configuration is used by:
AshDispatch.Changes.DispatchEventto find event modulesAshDispatch.Dispatcher.dispatch/2for manual event triggering- Manual trigger UI to list available events
User Resource
Specify your application's User resource for recipient resolution:
config :ash_dispatch,
user_resource: MyApp.Accounts.User,
user_domain: MyApp.AccountsWhy needed:
- Resolves
:useraudience to actual user records - Loads user relationships for template rendering
- Used by recipient resolution when dispatching events
Default: nil (must be configured for :user audience)
Preference Provider
Implement user notification preferences checking:
config :ash_dispatch,
preference_provider: MyApp.NotificationPreferencesYour preference provider must implement AshDispatch.PreferenceProvider behaviour:
defmodule MyApp.NotificationPreferences do
@behaviour AshDispatch.PreferenceProvider
@impl true
def allows_notification?(user_id, event_id, transport, opts) do
# Check if user allows this notification
# Return true/false
end
endWhy needed:
- Respects user opt-out preferences
- Required if you want user-configurable notifications
- Called before every delivery to
:useraudience
Default: nil (all notifications allowed)
See User Preferences for implementation guide.
Recipient Fields (Recipient Field Extraction)
Configure how recipient information is extracted for each transport:
config :ash_dispatch,
recipient_fields: [
email: [
identifier: :email,
name: [:contact_person, :display_name, :name]
],
in_app: [
identifier: :id,
name: [:contact_person, :display_name, :name]
]
]Why needed:
- Different transports need different recipient data (email address vs phone number vs user ID)
- Supports fallback chains for name fields
- Allows per-audience overrides for special cases
Transport-first structure: Each transport defines its own identifier (where to send) and name (who to display).
Fallback chains: For name fields, specify a list of fields to try in order:
name: [:display_name, :name, :contact_person] # tries each until one has a valuePer-audience overrides:
recipient_fields: [
email: [identifier: :email, name: :contact_person],
audiences: [
admin: [email: [name: :full_name]], # admins show full name
customer: [email: [identifier: :contact_email]] # customers use different email
]
]See Recipient Field Extraction for complete guide and advanced formats.
Notification Resource
Override the default notification resource with your own (for in-app notifications):
config :ash_dispatch,
notification_resource: MyApp.Notifications.NotificationWhy needed:
- Use your own Notification resource with custom fields
- Enable PubSub broadcasting for real-time updates
- Store notifications in Postgres instead of ETS
- Add custom policies and validations
Default: AshDispatch.Resources.Notification (ETS-based, no broadcasting)
Requirements for custom notification resource:
- Must accept these attributes in
:createaction::user_id(uuid):title(string):message(string):action_url(string, optional):event_id(string, optional):typeor:notification_type(atom: :info, :success, :warning, :error)
Example custom notification resource:
defmodule MyApp.Notifications.Notification do
use Ash.Resource,
domain: MyApp.Notifications,
data_layer: AshPostgres.DataLayer,
notifiers: [Ash.Notifier.PubSub]
postgres do
table "notifications"
repo MyApp.Repo
end
# Enable real-time broadcasting
pub_sub do
module MyAppWeb.Endpoint
prefix "notifications"
publish_all :create, ["user", :user_id]
publish_all :update, ["user", :user_id]
end
actions do
defaults [:read, :destroy]
create :create do
accept [
:user_id,
:type, # or :notification_type
:title,
:message,
:action_url,
:event_id
]
end
update :mark_as_read do
accept [:read, :read_at]
end
end
attributes do
uuid_v7_primary_key :id
attribute :user_id, :uuid do
allow_nil? false
end
attribute :type, :atom do
allow_nil? false
constraints one_of: [:info, :success, :warning, :error]
end
attribute :title, :string do
allow_nil? false
end
attribute :message, :string do
allow_nil? false
end
attribute :action_url, :string
attribute :event_id, :string
attribute :read, :boolean do
default false
end
attribute :read_at, :utc_datetime_usec
timestamps()
end
relationships do
belongs_to :user, MyApp.Accounts.User do
allow_nil? false
end
end
endOptional Configuration
Localization
Configure default locale for template resolution:
config :ash_dispatch,
default_locale: "sv", # Default when no locale found (defaults to "en")
gettext_backend: MyAppWeb.Gettext # Optional: translate content: strings via GettextWhy needed:
default_localesets the fallback language for templates and content translation- Used when no locale is configured on channel, event, or resource
- Templates fall back to non-localized versions if locale-specific not found
gettext_backend(optional) enables automatic translation ofcontent:block strings at dispatch time. See Localization for details.
Default: "en"
See Localization for the complete i18n guide including:
- Resource-level locale configuration
- Dynamic locale from record fields
- Template fallback chain
- Multi-language templates
Phoenix Channel Topic
Configure the channel topic prefix for real-time notifications:
config :ash_dispatch,
channel_topic: "inbox" # Creates topics like "inbox:user_id"Default: "user" (creates topics like "user:user_id")
Why needed:
- Must match your Phoenix channel topic structure
- Used for broadcasting notifications and counter updates
Ash Domains (for counter auto-discovery)
Specify which Ash domains to scan for counter definitions:
config :ash_dispatch,
domains: [MyApp.Orders, MyApp.Tickets, MyApp.Catalog, MyApp.Accounts]Why needed:
- CounterLoader scans these domains to discover counter definitions
- Required if you're using the counter broadcasting feature
- Counters defined in resources outside these domains won't be loaded
Default: [] (no domains scanned)
Recipient Resolver (Recommended)
Configure a declarative recipient resolver module for audience resolution:
config :ash_dispatch,
recipient_resolver: MyApp.RecipientResolverThe RecipientResolver module uses a DSL to define how recipients are resolved:
defmodule MyApp.RecipientResolver do
use AshDispatch.RecipientResolver,
user_resource: MyApp.Accounts.User
audiences do
audience :user, from_context: :user
audience :admins, query: [role: :admin, is_active: true]
audience :owner, resolve: :resolve_owner
audience :stakeholders, combine: [:owner, :team]
end
@impl true
def to_recipient(%MyApp.Accounts.User{} = user) do
%{id: user.id, email: to_string(user.email), display_name: user.full_name}
end
def resolve_owner(resource, context) do
# Custom resolution logic
end
endResolution Strategies:
| Strategy | Example | Description |
|---|---|---|
from_context | from_context: :user | Extract from context.data |
query | query: [role: :admin] | Query user_resource with Ash filter |
path | path: [:team, :users] | Follow relationship path on resource |
combine | combine: [:owner, :team] | Union of other audiences |
resolve | resolve: :resolve_owner | Custom resolver function |
See Recipient Resolution for the complete guide.
Audiences (Legacy Configuration)
Note: The
audiencesconfig is legacy. Prefer usingrecipient_resolverabove for new projects.
Configure how recipients are resolved for each audience type:
config :ash_dispatch,
audiences: [
# Bare atom = relationship-based (extract from record)
:user, # Extract from :user relationship
:creator, # Extract from :creator relationship
:partner, # Extract from :partner relationship
# Tuple = filter-based (query all matching users)
{:admin, [:user, {:admin, true}]},
{:super_admin, [:user, {:super_admin, true}]},
{:support, [:user, {:role, :support}]}
]Two configuration patterns:
| Pattern | Format | Behavior |
|---|---|---|
| Relationship-based | :user | Extract from record's relationship |
| Filter-based | {:admin, [...]} | Query all users matching filter |
Default: [] (assumes relationship-based for unknown audiences)
See Counter Broadcasting and Recipient Resolution for details.
Base URL
Set the base URL for generating links in notifications:
config :ash_dispatch,
base_url: "https://app.example.com"Default: "http://localhost:4000"
Used by:
- Link generation in emails
- Action URLs in notifications
- Available in context as
context.base_url
Email From Address
Default sender email address:
config :ash_dispatch,
default_from_email: {"My App Support", "support@example.com"}Default: {"AshDispatch", "noreply@example.com"}
Note: Event modules can override this per-event via from/2 callback.
Delivery Receipt Resource
Override the default delivery receipt resource:
config :ash_dispatch,
delivery_receipt_resource: MyApp.Deliveries.DeliveryReceiptDefault: AshDispatch.Resources.DeliveryReceipt
Why override:
- Add custom fields for your use case
- Integrate with existing audit/logging systems
- Custom policies or validations
Send Now Authorizer
Configure authorization for the send_now action on delivery receipts. This action allows manually triggering email delivery for scheduled receipts from an admin UI.
config :ash_dispatch,
send_now_authorizer: MyApp.Deliveries.SendNowAuthorizerDefault: nil (any authenticated actor can use send_now)
Why needed:
- Restrict manual email triggering to specific user roles (e.g., super admins only)
- Prevent accidental mass email sends by regular admins
- Audit control over who can bypass normal scheduling
Implementing an authorizer:
Your authorizer module must implement an authorize/1 function that receives the actor and returns :ok or {:error, message}:
defmodule MyApp.Deliveries.SendNowAuthorizer do
@moduledoc """
Authorizes send_now action for delivery receipts.
Only super admins can manually trigger email sending.
"""
@doc """
Check if actor is authorized to use send_now.
Returns:
- `:ok` if authorized
- `{:error, message}` if not authorized
"""
def authorize(%{super_admin: true}), do: :ok
def authorize(_actor), do: {:error, "Only super admins can manually trigger email sending"}
endBehavior:
- When
nil: Any authenticated actor can usesend_now - When configured: The authorizer is called with the actor
- System calls (no actor): Always allowed regardless of authorizer
Important: The actor must be passed to Ash.Changeset.for_update/3 for the authorizer to receive it:
# Correct - actor passed to for_update
receipt
|> Ash.Changeset.for_update(:send_now, %{}, actor: current_user)
|> Ash.update(authorize?: true)
# Incorrect - actor won't reach the validation
receipt
|> Ash.Changeset.for_update(:send_now, %{})
|> Ash.update(actor: current_user, authorize?: true)Audience Resolution
Configure how recipients are resolved for each audience using recipient_filters:
config :ash_dispatch,
recipient_filters: [
audiences: [
# Bare atom: extract from relationship with same name
:user,
:creator,
# Filter-based: query users matching filter
admin: [:user, admin: true],
partner: [:user, role: :partner],
# Relationship chain: follow multiple relationships
seller: [:user, :associated_seller]
]
]This replaces the need for custom resolver modules. See Recipient Resolution for all 6 supported audience formats and advanced examples.
Complete Example
Here's a complete configuration with all features enabled:
# config/config.exs
config :ash_dispatch,
# Required: Your app's OTP name (for event auto-discovery)
otp_app: :my_app,
# Counter broadcasting (real-time updates)
counter_broadcast_fn: {MyAppWeb.UserChannel, :broadcast_counter},
domains: [MyApp.Orders, MyApp.Tickets, MyApp.Catalog, MyApp.Accounts],
user_module: MyApp.Accounts.User,
# Audience configuration (unified format)
audiences: [
# Relationship-based (extract from record)
:user,
:partner,
# Filter-based (query all matching users)
{:admin, [:user, {:admin, true}]},
{:super_admin, [:user, {:super_admin, true}]}
],
# Event modules are AUTO-DISCOVERED from domains - no manual config needed!
# The EventRegistry scans resources with AshDispatch.Resource extension
# User/admin resolution
user_resource: MyApp.Accounts.User,
user_domain: MyApp.Accounts,
# Custom resources
notification_resource: MyApp.Notifications.Notification,
delivery_receipt_resource: MyApp.Deliveries.DeliveryReceipt,
# Action authorization (restrict send_now to super admins)
send_now_authorizer: MyApp.Deliveries.SendNowAuthorizer,
# User preferences
preference_provider: MyApp.NotificationPreferences,
# Email configuration
email_backend: AshDispatch.EmailBackend.Swoosh,
swoosh_mailer: MyApp.Mailer,
default_from_email: {"My App", "noreply@myapp.com"},
# Base URL
base_url: "https://app.myapp.com"
# Swoosh configuration
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.Resend,
api_key: System.get_env("RESEND_API_KEY")
# Oban configuration
config :my_app, Oban,
engine: Oban.Engines.Basic,
notifier: Oban.Notifiers.Postgres,
repo: MyApp.Repo,
queues: [emails: 10]Environment-Specific Configuration
Development
# config/dev.exs
config :ash_dispatch,
base_url: "http://localhost:4000"
# Use local email adapter
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.LocalTest
# config/test.exs
config :ash_dispatch,
base_url: "http://localhost:4002"
# Use test adapter
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.TestProduction
# config/runtime.exs
config :ash_dispatch,
base_url: System.get_env("APP_URL") || "https://app.example.com"
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.Resend,
api_key: System.get_env("RESEND_API_KEY")Configuration Validation
AshDispatch validates configuration at compile time and runtime. You'll see warnings if:
- Email backend configured but Swoosh not available
- User resource configured but not found
- Notification resource missing required attributes
Broadcast Transport
The :broadcast transport provides lightweight PubSub broadcasting for real-time events that don't need receipt tracking or notification records.
Admin Channel Topic
Configure the admin/firehose PubSub topic:
config :ash_dispatch,
admin_channel_topic: "admin:firehose"Default: "admin:firehose"
How It Works
- User audience: Broadcasts to
"{channel_topic}:{user_id}"— same topic prefix as:in_apptransport - Admin audience: Broadcasts to the
admin_channel_topic - No receipts: Skips DeliveryReceipt creation for zero DB overhead — suitable for high-frequency streaming events
- Config-driven: Uses
pubsub_moduleandchannel_topicfrom your existing config
Example
dispatch do
event :chat_chunk,
trigger_on: :manual,
channels: [[transport: :broadcast, audience: :user]],
metadata: [category: :streaming]
endDispatch manually from your pipeline:
AshDispatch.Dispatcher.dispatch("my_resource.chat_chunk", %{
user: %{id: user_id},
content: chunk_text,
session_id: session_id
})Next Steps
- Getting Started - Basic setup
- Delivery Receipts - Receipt management and admin actions
- User Preferences - Implement preference checking
- Oban Configuration - Configure job queue