Generators & Setup

View Source

AshDispatch provides generators to integrate with your application. Use mix ash_dispatch.setup once for initial setup, then define events in your resource DSL and run mix ash_dispatch.gen to generate missing files.

Quick Start

# Initial setup (creates layouts and directory structure)
mix ash_dispatch.setup

# Generate missing templates, event modules, and TypeScript types
mix ash_dispatch.gen

# Or use the unified Ash codegen command
mix ash.codegen

Setup Task

Run once when first integrating AshDispatch:

mix ash_dispatch.setup

What It Creates

priv/ash_dispatch/
 layouts/
    email.html.heex    # Your branded email layout
    email.text.eex     # Plain text email layout
 templates/             # Your event templates go here

Customizing Layouts

After setup, edit the layouts to match your brand:

<!-- priv/ash_dispatch/layouts/email.html.heex -->
<!DOCTYPE html>
<html>
  <head><title><%= @subject %></title></head>
  <body>
    <!-- Your header with logo, brand colors -->
    <header>
      <img src="your-logo.png" />
      <h1><%= @subject %></h1>
    </header>

    <!-- Event content injected here -->
    <%= @inner_content %>

    <!-- Your footer with contact info -->
    <footer>
      Your Company | support@example.com
    </footer>
  </body>
</html>

All event templates will automatically use this layout.


Defining Events in DSL

Events are defined directly in your resource using the dispatch do block:

defmodule MyApp.Orders.ProductOrder do
  use Ash.Resource,
    extensions: [AshDispatch.Resource]

  dispatch do
    event :created do
      module MyApp.Orders.Events.Created.Event
      trigger_on [:create]
      data_key :order

      channels do
        channel :in_app, :user,
          title: "Order skapad",
          message: "Din order har registrerats"
        channel :email, :user,
          subject: "Din order har skapats"
        channel :email, :admin,
          variant: :admin,
          subject: "Ny order inkommit"
      end
    end
  end
end

DSL Options

OptionDescription
moduleEvent module that handles template assigns and recipients
trigger_onActions that trigger this event (:create, :update, etc.)
data_keyKey used for the resource in template assigns
channelsList of delivery channels (in-app, email, SMS, etc.)

Channel Options

OptionDescription
:in_appCreates a notification in the database
:emailSends an email using templates
audience:user, :admin, or :system
variantTemplate variant (e.g., :admin uses email.admin.html.heex)
titleIn-app notification title
messageIn-app notification message
subjectEmail subject line

Generating Missing Files

After defining events in DSL, run the generator to create missing templates and event modules:

# Generate all missing files
mix ash_dispatch.gen

# Or via unified Ash codegen
mix ash.codegen

# Preview what would be generated
mix ash_dispatch.gen --dry-run

# CI check - fail if files need generation
mix ash_dispatch.gen --check

What Gets Generated

For each event with an :email channel, the generator creates:

lib/my_app/orders/events/created/
 event.ex                 # Event module (if module option specified but file missing)
 templates/
     email.html.heex      # HTML email content
     email.text.eex       # Plain text email content
     email.admin.html.heex  # If variant: :admin channel exists
     email.admin.text.eex

Content-Only Templates

Templates only contain event-specific content. The layout (from priv/ash_dispatch/layouts/) handles structure:

<!-- lib/my_app/orders/events/created/templates/email.html.heex -->
<p>Hej <strong><%= @display_name %></strong>,</p>

<p>Tack för din beställning!</p>

<div style="background: #f0f9ff; padding: 20px;">
  <h2>Orderdetaljer</h2>
  <p>Ordernummer: <%= @order_number %></p>
</div>

<a href={@source_url}>Visa order →</a>

No DOCTYPE, html, head, body, header, or footer needed - the layout provides all that.


Transport-Specific Layouts

Layouts are discovered automatically by transport name. Each transport can have its own layout and format.

How It Works

priv/ash_dispatch/layouts/
 email.html.heex     # HTML layout for :email transport
 email.text.eex      # Text layout for :email transport
 sms.text.eex        # Text layout for :sms transport (if added)
 discord.md.eex      # Markdown layout for :discord transport (if added)

The template resolver looks for: {transport}.{extension} where extension comes from format.

Format Extensions

Configure custom formats for new transports:

# config/config.exs
config :ash_dispatch,
  format_extensions: %{
    # Defaults (already included):
    # html: "html.heex",
    # text: "text.eex",

    # Custom formats:
    markdown: "md.eex"     # For Discord/Slack
    # json: "json.eex"     # For webhooks
  }

Adding a New Transport (e.g., SMS)

  1. Create layout at priv/ash_dispatch/layouts/sms.text.eex:

    <%= @subject %>
    
    <%= @inner_content %>
    
    ---
    Sent from MyApp
  2. Create templates for each event at priv/ash_dispatch/templates/order/created/sms.text.eex:

    Order <%= @order_number %> confirmed!
    Track at: <%= @order_url %>
  3. Add channel to your event:

    channels: [
    [transport: :sms, audience: :user, format: :text]
    ]

Adding Discord with Markdown

  1. Add format config:

    config :ash_dispatch,
    format_extensions: %{markdown: "md.eex"}
  2. Create layout at priv/ash_dispatch/layouts/discord.md.eex:

    **<%= @subject %>**
    
    <%= @inner_content %>
  3. Create templates at priv/ash_dispatch/templates/order/created/discord.md.eex:

    🎉 New order **<%= @order_number %>** from <%= @display_name %>!

Custom Layouts Per Channel

Override the layout for specific channels using the layout option:

channels: [
  # Default layout
  [transport: :email, audience: :user],

  # Custom "urgent" layout for cancelled orders
  [transport: :email, audience: :user, layout: "urgent"],

  # Discord still uses default layout
  [transport: :discord, audience: :admin]
]

Layout structure:

priv/ash_dispatch/layouts/
 email.html.heex          # Default email layout
 email.text.eex           # Default text layout
 discord.md.eex           # Default Discord layout
 urgent/
     email.html.heex      # Urgent email (red header)
     email.text.eex       # Urgent text

The layout option is a subdirectory prefix. It tries layouts/urgent/email.html.heex first, then falls back to layouts/email.html.heex if not found.

Use cases:

  • Warning/error emails with different header colors
  • Admin notifications with different branding
  • Promotional emails with special styling

TypeScript SDK Generation

The mix ash_dispatch.gen command generates a complete TypeScript SDK based on your DSL definitions:

# Generate all missing files (templates, event modules, SDK)
mix ash_dispatch.gen

# Preview what would be generated
mix ash_dispatch.gen --dry-run

# CI check - fail if files need generation
mix ash_dispatch.gen --check

What Gets Generated

TypeScript SDK (8 files)

The generator creates a complete TypeScript SDK for real-time updates:

lib/ash-dispatch/
 index.ts              # Re-exports everything
 types.ts              # Counter types, defaults, metadata, accessors
 events.ts             # Event ID types and metadata
 store.ts              # Zustand store for counters
 channel.ts            # Phoenix channel utilities
 hooks/
     use-channel.ts    # WebSocket connection hook
     use-counter.ts    # Single counter access
     use-notifications.ts  # Notification management

Counter Types (types.ts)

Type-safe counter definitions for your frontend:

// Generated types.ts
export type CounterName =
  | "pending_orders"
  | "cart_items"
  | "unread_notifications"
  | "admin_pending_requests"

export const DEFAULT_COUNTERS: AllCounters = {...}
export const COUNTER_METADATA = {...}

export function isValidCounter(name: string): name is CounterName
export function getCounterAccessors(counters: AllCounters): CounterAccessors

Event Types (events.ts)

Type-safe event definitions for frontend event handling:

// Generated events.ts
export type EventId =
  | "orders.created"
  | "orders.completed"
  | "tickets.created"

export const EVENT_METADATA = {
  "orders.created": {
    domain: "orders",
    channels: [{ transport: "email", audience: "user" }],
  },
  // ...
} as const;

export function isValidEventId(id: string): id is EventId

Event Module Stubs

If an event specifies a module option but the file doesn't exist, the generator creates a stub:

defmodule MyApp.Orders.Events.Created.Event do
  use AshDispatch.Event

  # Override callbacks to customize behavior:
  # @impl true
  # def prepare_template_assigns(context, channel) do
  #   %{order_number: context.data.order.order_number}
  # end
end

Configuration

Required Configuration

# config/config.exs

# Path derivation from ash_typescript
config :ash_typescript,
  output_file: "apps/frontend/src/lib/ash_rpc.ts"

# SDK will generate to: apps/frontend/src/lib/ash-dispatch/

Optional Configuration

config :ash_dispatch,
  # Override SDK output path
  sdk_output_path: "apps/frontend/src/lib/ash-dispatch",

  # Domains to scan for counters/events
  domains: [MyApp.Orders, MyApp.Tickets, MyApp.Notifications]

Usage

Full Generation

Generate everything at once:

mix ash_dispatch.gen

Output:

* creating apps/frontend/src/lib/ash-dispatch/types.ts
* creating apps/frontend/src/lib/ash-dispatch/events.ts
* creating apps/frontend/src/lib/ash-dispatch/store.ts
* creating apps/frontend/src/lib/ash-dispatch/channel.ts
* creating apps/frontend/src/lib/ash-dispatch/index.ts
* creating apps/frontend/src/lib/ash-dispatch/hooks/use-channel.ts
* creating apps/frontend/src/lib/ash-dispatch/hooks/use-counter.ts
* creating apps/frontend/src/lib/ash-dispatch/hooks/use-notifications.ts

Generated 8 file(s)

Incremental Updates

The generator only creates missing files. When you add new counters or events to your DSL, run mix ash_dispatch.gen again to update types.ts and events.ts:

# After adding new counters/events
mix ash_dispatch.gen

# Output shows only changed files:
* creating apps/frontend/src/lib/ash-dispatch/types.ts

Generated 1 file(s)

Generated SDK Usage

Frontend Setup

// app/providers.tsx
import { useChannel, useCounterStore } from '@/lib/ash-dispatch'

export function Providers({ children }) {
  const userChannel = useUserChannel(userId)

  useChannel({
    channel: userChannel,
    onNotification: (notification) => {
      toast.success(notification.title)
    }
  })

  return <>{children}</>
}

Using Counters

import { useCounter } from '@/lib/ash-dispatch'

function CartIcon() {
  const cartItems = useCounter('cart_items')

  return (
    <Badge count={cartItems}>
      <ShoppingCart />
    </Badge>
  )
}

Using the Store Directly

import { useCounterStore } from '@/lib/ash-dispatch'

function Dashboard() {
  const counters = useCounterStore(state => state.counters)

  return (
    <div>
      <span>Orders: {counters.pending_orders}</span>
      <span>Tickets: {counters.active_tickets}</span>
    </div>
  )
}

Type Safety

The generated types ensure you only use valid counter names:

import type { CounterName } from '@/lib/ash-dispatch'

// ✅ Type-safe
const count = useCounter('cart_items')

// ❌ Type error: "invalid_counter" is not assignable to CounterName
const invalid = useCounter('invalid_counter')

Backend Integration

For minimal backend setup, use the UserChannel macro:

defmodule MyAppWeb.UserChannel do
  use AshDispatch.Phoenix.UserChannel,
    endpoint: MyAppWeb.Endpoint
end

That's it! 3 lines for a complete real-time channel with:

  • Counter broadcasting
  • Notification pushing
  • Initial state loading

See Phoenix Integration for customization options.


Counter Discovery

The generator automatically discovers counters from your resource DSL:

# In your resource
defmodule MyApp.Orders.ProductOrder do
  use Ash.Resource, extensions: [AshDispatch.Resource]

  dispatch do
    counters do
      counter :pending_orders,
        trigger_on: [:create, :complete],
        query_filter: [status: :pending],
        audience: :user
    end
  end
end

These become TypeScript types and Zustand store keys automatically.


Workflow Integration

Development Flow

  1. Define counters in resource DSL
  2. Run mix ash_dispatch.gen
  3. Import hooks in frontend
  4. Use counters with full type safety

CI/CD

Add to your build pipeline:

- name: Generate AshDispatch SDK
  run: mix ash_dispatch.gen --check

The --check flag exits with error if any files need generation, ensuring your SDK stays in sync with DSL definitions.


Troubleshooting

"No counters found"

Cause: No resources with counter definitions in configured domains.

Solution: Ensure domains are configured:

# config/config.exs
config :my_app,
  ash_domains: [MyApp.Orders, MyApp.Tickets]

# Or explicitly in ash_dispatch
config :ash_dispatch,
  domains: [MyApp.Orders, MyApp.Tickets]

SDK path incorrect / not generating

Cause: ash_typescript.output_file not configured.

Solution: Set the output path:

config :ash_typescript,
  output_file: "apps/frontend/src/lib/ash_rpc.ts"

The SDK will generate to apps/frontend/src/lib/ash-dispatch/.

Templates not generating

Cause: Events don't have :email channels defined, or the templates already exist.

Solution: Ensure your event DSL includes email channels:

dispatch do
  event :created do
    channels do
      channel :email, :user, subject: "Order created"
    end
  end
end

Next Steps