This guide is the canonical adoption path for composing Chimeway with Mailglass. Follow it when you want one credible vertical slice: add both libraries, configure the Mailglass adapter, trigger an email delivery, inspect the trace, and optionally wire inbound feedback.
Responsibility split (SEED-003)
Chimeway orchestrates the when and why: durable notification lifecycle, suppression and preference gates, idempotency, scheduling, and operator traces you can search at /admin/chimeway.
Mailglass handles templating and delivery: MJML templates, Swoosh email assembly, and provider send. Chimeway passes notifier rendering/2 assigns through to your host mailable; Mailglass builds the final message.
Product name vs module: REQUIREMENTS and adoption docs refer to Chimeway.Adapter.Mailglass; the implementation module is Chimeway.Adapters.Mailglass.
For copy-paste notifier and adapter sections, see the Mailglass integration blueprint. This guide owns the end-to-end path from dependency to verification.
1. Dependencies
Add Chimeway and Mailglass to your host mix.exs:
def deps do
[
{:chimeway, "~> 1.0"},
{:mailglass, "~> 1.3"}
]
endThen fetch dependencies:
mix deps.get
2. Database / migrations
Chimeway stores the durable lifecycle spine (event → notification → delivery → attempt) in your database. Generate and run Chimeway migrations:
mix chimeway.gen.migrations
mix ecto.migrate
For Chimeway install depth — repo config, supervisor setup, and migration idempotency — see Installation.
Mailglass maintains its own schema and repo. Follow the Mailglass installation docs for repo setup, migrations, and Oban queues in your host application. Both libraries typically share the same Postgres database but use separate Ecto repos.
3. Runtime config
Register the Mailglass adapter for the email channel and map render_key values to host mailable functions:
config :chimeway,
channel_adapters: %{"email" => Chimeway.Adapters.Mailglass},
channel_adapter_configs: %{
"email" => [
mailables: %{
"teampulse.invite_sent.email" => {DemoHost.Mailers.InviteEmail, :invite_email}
}
]
}Replace DemoHost.Mailers.InviteEmail with your host mailable module in production apps. The render_key string in notifier rendering/2 must match the key in the mailables map.
Configure Mailglass per its docs — repo, Swoosh adapter, and provider credentials. Chimeway does not manage Mailglass application config; it only invokes your mailable through the adapter at delivery time.
For the full Chimeway runtime setup (installer repo, Chimeway.Repo, supervisor), see Installation §3–§4.
4. Host mailable
Your host owns the Mailglass.Mailable module. Chimeway's Mailglass adapter resolves render_key → {Module, :function} and passes delivery render_data (notifier assigns plus recipient "to") into the mailable function.
Example host mailable for the teampulse.invite_sent.email render key:
defmodule DemoHost.Mailers.InviteEmail do
use Mailglass.Mailable, stream: :transactional
def invite_email(assigns) when is_map(assigns) do
to = Map.get(assigns, "to") || Map.get(assigns, :to)
subject = Map.get(assigns, "subject") || "You're invited"
html_body = Map.get(assigns, "html_body") || ""
text_body = Map.get(assigns, "text_body") || ""
new()
|> Mailglass.Message.update_swoosh(fn email ->
email
|> Swoosh.Email.to(to)
|> Swoosh.Email.from({"TeamPulse", "invites@teampulse.test"})
|> Swoosh.Email.subject(subject)
|> Swoosh.Email.html_body(html_body)
|> Swoosh.Email.text_body(text_body)
end)
|> Mailglass.Message.put_function(:invite_email)
end
endPair this mailable with a notifier that declares stable keys and a matching render_key:
defmodule DemoHost.Notifiers.InviteSent do
use Chimeway.Notifier
@impl true
def notification_key, do: "teampulse.invite_sent"
@impl true
def version, do: 1
@impl true
def recipients(%{email: email}) do
{:ok, [%{recipient_identity: "user:#{email}", recipient_type: "user"}]}
end
@impl true
def build(%{team_name: team_name}, _recipient) do
{:ok, %{"headline" => "You're invited to #{team_name}", "body" => "Join your team."}}
end
@impl true
def channels(_params, _recipient), do: {:ok, [:email, :in_app]}
@impl true
def rendering(%{team_name: team_name}, _recipient) do
{:ok,
%{
assigns: %{
"subject" => "You're invited to #{team_name}",
"html_body" => "<p>Join your team.</p>",
"text_body" => "Join your team."
},
channels: %{
email: %{render_key: "teampulse.invite_sent.email", render_version: 1}
}
}}
end
endRunnable reference: DemoHost.Notifiers.InviteSent and DemoHost.Mailers.InviteEmail in the demo host.
5. Trigger and verification
Trigger the notifier with required idempotency and tenancy:
Chimeway.trigger(
DemoHost.Notifiers.InviteSent,
%{email: "alex@teampulse.test", team_name: "Engineering"},
idempotency_key: "teampulse-invite-alex",
tenant_id: "teampulse"
)Both :idempotency_key and :tenant_id are required. Omitting tenant_id returns {:error, :missing_tenant_id}.
After delivery, verify explainability:
- Search
/admin/chimewayby recipient identity — the delivery detail shows the stable notification key (teampulse.invite_sent) and the Mailglass adapter module on the attempt timeline. - In IEx, use
Chimeway.Traces.explain_delivery/1on a delivery ID from the trigger result.
Runnable demo: DemoHost.Seeds.seed_invite/0 triggers the same notifier with deterministic idempotency keys for local proof.
As a named proof command, run mix verify.mailglass after wiring — it exercises the Mailglass adapter contract, executor routing, webhook pipeline, and demo host delivery proof.
6. Optional inbound feedback
When provider webhooks should drive workflow progression, mount inbound feedback through Chimeway.Webhooks.process/4 in your host controller. Do not bypass Chimeway's ingress layer with a standalone Mailglass webhook plug — Chimeway owns ingress durability, attempt recording, and signal emission; the Mailglass adapter supplies webhook callbacks (verify_webhook, resolve_delivery, normalize_feedback) behind the adapter behaviour.
Example host route (optional demo path /webhooks/chimeway/mailglass):
def create(conn, _params) do
# Pitfall 4 / T-33-RAWBODY: flatten cached iolist chunks to binary before HMAC verify.
raw_body =
conn.assigns
|> Map.get(:raw_body, [])
|> Enum.reverse()
|> IO.iodata_to_binary()
headers = conn.req_headers
adapter_module = Chimeway.Adapters.Mailglass
config = Application.get_env(:my_app, :chimeway_webhook_config, [])
case Chimeway.Webhooks.process(adapter_module, raw_body, headers, config) do
{:ok, _ingress} ->
send_resp(conn, 200, "OK")
{:error, :unauthorized} ->
send_resp(conn, 401, "Unauthorized")
{:error, _other} ->
send_resp(conn, 500, "Internal Server Error")
end
endHosts using a custom :body_reader must cache raw bytes in conn.assigns[:raw_body] before parsers consume the body — see DemoHost.Plugs.CacheBodyReader and the runnable reference at examples/chimeway_demo_host/lib/demo_host_web/controllers/webhooks_controller.ex.
Log error reasons server-side only; never return internal error tuples to the webhook provider (Phase 33 D-03). Hosts MAY use 400 or 422 for observability, but MUST return non-2xx for library errors so providers retry.
The adapter's verify_webhook/3 validates the provider signature, resolve_delivery/2 maps the payload to a Chimeway delivery row, and normalize_feedback/1 converts provider events into canonical delivery outcomes.
For workflow progression context — how chimeway.delivery.succeeded and chimeway.delivery.bounced signals resume or stop runs — see Feedback escalation workflow.
Related guides
- Golden Path — Chimeway-only first integration
- Mailglass integration blueprint — focused notifier/adapter recipe
- Custom adapter — adapter behaviour and Mailglass stub
- Feedback escalation workflow — webhook-driven workflow progression
- Installation — Chimeway install and migration depth