# Configuration
AshDispatch 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:
```elixir
# config/config.exs
config :ash_dispatch,
counter_broadcast_fn: {MyAppWeb.UserChannel, :broadcast_counter}
```
**Options:**
```elixir
# MFA tuple (recommended - easier to test)
counter_broadcast_fn: {MyAppWeb.UserChannel, :broadcast_counter}
# Or function capture
counter_broadcast_fn: &MyAppWeb.UserChannel.broadcast_counter/4
```
**Your broadcast function should accept:**
- `user_id` (string) - The user to broadcast to
- `counter_name` (atom) - The counter name (e.g., `:pending_orders`)
- `value` (integer) - The current count
- `opts` (keyword list) - Options including `:metadata` with `invalidate_queries`
**Example implementation:**
```elixir
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
end
```
See [Phoenix Channel Integration](phoenix-integration.md) for complete setup guide.
### Email Backend (if using email transport)
Configure Swoosh for email delivery:
```elixir
# 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:
```elixir
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](oban-configuration.md) 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:
1. Events with explicit `module:` option in DSL
2. Auto-generated event modules following the naming convention
```elixir
# 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 event
- `EventRegistry.find_module("order.created")` - Find event module
This configuration is used by:
- `AshDispatch.Changes.DispatchEvent` to find event modules
- `AshDispatch.Dispatcher.dispatch/2` for manual event triggering
- Manual trigger UI to list available events
### User Resource
Specify your application's User resource for recipient resolution:
```elixir
config :ash_dispatch,
user_resource: MyApp.Accounts.User,
user_domain: MyApp.Accounts
```
**Why needed:**
- Resolves `:user` audience 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:
```elixir
config :ash_dispatch,
preference_provider: MyApp.NotificationPreferences
```
Your preference provider must implement `AshDispatch.PreferenceProvider` behaviour:
```elixir
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
end
```
**Why needed:**
- Respects user opt-out preferences
- Required if you want user-configurable notifications
- Called before every delivery to `:user` audience
**Default:** `nil` (all notifications allowed)
See [User Preferences](user-preferences.md) for implementation guide.
### Recipient Fields (Recipient Field Extraction)
Configure how recipient information is extracted for each transport:
```elixir
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:
```elixir
name: [:display_name, :name, :contact_person] # tries each until one has a value
```
**Per-audience overrides:**
```elixir
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](recipient-extractor.md) for complete guide and advanced formats.
### Notification Resource
Override the default notification resource with your own (for in-app notifications):
```elixir
config :ash_dispatch,
notification_resource: MyApp.Notifications.Notification
```
**Why 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 `:create` action:
- `:user_id` (uuid)
- `:title` (string)
- `:message` (string)
- `:action_url` (string, optional)
- `:event_id` (string, optional)
- `:type` or `:notification_type` (atom: :info, :success, :warning, :error)
**Example custom notification resource:**
```elixir
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
end
```
## Optional Configuration
### Localization
Configure default locale for template resolution:
```elixir
config :ash_dispatch,
default_locale: "sv", # Default when no locale found (defaults to "en")
gettext_backend: MyAppWeb.Gettext # Optional: translate content: strings via Gettext
```
**Why needed:**
- `default_locale` sets 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 of `content:` block strings at dispatch time. See [Localization](localization.md#gettext-integration-content-string-translation) for details.
**Default:** `"en"`
See [Localization](localization.md) 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:
```elixir
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:
```elixir
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:
```elixir
config :ash_dispatch,
recipient_resolver: MyApp.RecipientResolver
```
The `RecipientResolver` module uses a DSL to define how recipients are resolved:
```elixir
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
end
```
**Resolution 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](recipient-resolution.md) for the complete guide.
### Audiences (Legacy Configuration)
> **Note:** The `audiences` config is legacy. Prefer using `recipient_resolver` above for new projects.
Configure how recipients are resolved for each audience type:
```elixir
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](counter-broadcasting.md) and [Recipient Resolution](recipient-resolution.md) for details.
### Base URL
Set the base URL for generating links in notifications:
```elixir
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:
```elixir
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:
```elixir
config :ash_dispatch,
delivery_receipt_resource: MyApp.Deliveries.DeliveryReceipt
```
**Default:** `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.
```elixir
config :ash_dispatch,
send_now_authorizer: MyApp.Deliveries.SendNowAuthorizer
```
**Default:** `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}`:
```elixir
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"}
end
```
**Behavior:**
- When `nil`: Any authenticated actor can use `send_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:
```elixir
# 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`:
```elixir
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](recipient-resolution.md)
for all 6 supported audience formats and advanced examples.
## Complete Example
Here's a complete configuration with all features enabled:
```elixir
# 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
```elixir
# config/dev.exs
config :ash_dispatch,
base_url: "http://localhost:4000"
# Use local email adapter
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.Local
```
### Test
```elixir
# config/test.exs
config :ash_dispatch,
base_url: "http://localhost:4002"
# Use test adapter
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.Test
```
### Production
```elixir
# 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:
```elixir
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_app` transport
- **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_module` and `channel_topic` from your existing config
### Example
```elixir
dispatch do
event :chat_chunk,
trigger_on: :manual,
channels: [[transport: :broadcast, audience: :user]],
metadata: [category: :streaming]
end
```
Dispatch manually from your pipeline:
```elixir
AshDispatch.Dispatcher.dispatch("my_resource.chat_chunk", %{
user: %{id: user_id},
content: chunk_text,
session_id: session_id
})
```
## Next Steps
- [Getting Started](../tutorials/getting-started.md) - Basic setup
- [Delivery Receipts](delivery-receipts.md) - Receipt management and admin actions
- [User Preferences](user-preferences.md) - Implement preference checking
- [Oban Configuration](oban-configuration.md) - Configure job queue