This guide wires mailglass 2.x into a Phoenix app and sends one message through the public package API, without repository fixtures or private modules.
Prerequisites
- Elixir
~> 1.18with OTP 27+ - Phoenix
~> 1.8 - Ecto and PostgreSQL configured
- Swoosh adapter credentials in your runtime environment
1) Install and verify
mix deps.get
mix mailglass.install
mix ecto.migrate
mix compile --warnings-as-errors
2) Configure mailglass
# config/runtime.exs
config :mailglass,
repo: MyApp.Repo,
adapter:
{Mailglass.Adapters.Swoosh,
swoosh_adapter: {Swoosh.Adapters.Postmark, api_key: System.fetch_env!("POSTMARK_API_KEY")}},
renderer: [plaintext: true, css_inliner: :premailex],
telemetry: [default_logger: true]renderer.plaintext controls only automatic plaintext for HTML-only mail:
true generates it and false leaves it absent. Nonblank authored plaintext
always wins. renderer.css_inliner: :premailex inlines CSS; :none skips only
that step, while HEEx rendering and data-mg-* stripping still run.
3) Mount preview and webhook routes
# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use Phoenix.Router
import MailglassAdmin.Router
import Mailglass.Webhook.Router
if Application.compile_env(:my_app, :dev_routes) do
scope "/dev" do
pipe_through :browser
mailglass_admin_routes "/mail"
end
end
scope "/" do
pipe_through :api
mailglass_webhook_routes "/webhooks"
end
end4) Send your first message
defmodule MyApp.UserMailer do
use Mailglass.Mailable, stream: :transactional
def welcome(user) do
new()
|> to(user.email)
|> from({"MyApp", "support@example.com"})
|> subject("Welcome")
|> html_body("<h1>Welcome to MyApp</h1>")
|> text_body("Welcome to MyApp")
|> Mailglass.Message.put_function(:welcome)
end
end
{:ok, _delivery} =
%{email: "alice@example.com"}
|> MyApp.UserMailer.welcome()
|> Mailglass.deliver()This message has exactly one recipient: one native to recipient and both nonblank HTML and plaintext
bodies. With the default Mailglass.Tenancy.SingleTenant resolver, no tenant
stamp is required: the shared preflight records ownership as string "default".
cc and bcc count toward the same one-recipient total, and a sole recipient
in any one of those native fields is supported. The complete supported async
envelope is private durable state; public Delivery metadata is never used to
reconstruct provider input.
To select the configured asynchronous path instead, call:
%{email: "alice@example.com"}
|> MyApp.UserMailer.welcome()
|> Mailglass.deliver_later()async_adapter: :task_supervisor is explicitly non-durable and intended for
development or test use. The default async_adapter: :oban is durable: it
either atomically records the Delivery, queued event, private transport state,
and one :mailglass_outbound job, or returns a typed error without queued work.
It never automatically changes to TaskSupervisor. Before production go-live,
configure the canonical queue and run Mailglass.Config.production_readiness/0.
The private transport state is not a sent-message archive, payload viewer, or
public Payload API. Any queued Delivery without that private Payload settles as
legacy_payload_missing, preserves public Delivery/Event history, and never
invokes the adapter. To send it later, an operator must re-author/re-enqueue it
from an authoritative private source; new jobs carry exactly delivery_id and
mailglass_tenant_id.
End-to-End Example
mix deps.get
mix mailglass.install
mix ecto.migrate
mix compile --warnings-as-errors
Troubleshooting the Installer
mix mailglass.install fails to find endpoint.ex
- Ensure you are running the task from the root of your Phoenix application.
- If your application has a non-standard directory structure, you may need to manually wire the components described in the Webhooks Guide.
Webhooks return 401 after installation
mix mailglass.install now fails closed when it detects an unmanaged Plug.Parsers plug
(one that lacks a body_reader option) in your endpoint.ex. It exits non-zero via
Mix.raise and prints an actionable error — this prevents the silent production webhook
401 that occurred with earlier versions.
If the installer stopped with a conflict error:
- Re-run with
--forceto bypass the check and proceed:mix mailglass.install --force. The installer will insert the managedPlug.Parsersblock (withbody_reader: {Mailglass.Webhook.CachingBodyReader, :read_body, []}) above your existing parser. Use--forceonly if you understand the parser ordering implications. - After a successful install, run
mix mailglass.doctorto confirm theMailglass.Webhook.CachingBodyReaderwiring is present in your endpoint parser. The command exits non-zero if the wiring is absent.
If you installed mailglass with an older installer:
Earlier installer versions warned instead of failing closed on this conflict. Check
lib/my_app_web/endpoint.ex manually: if your Plug.Parsers block lacks a body_reader
option, add the CachingBodyReader wiring as shown in the Webhooks guide,
then run mix mailglass.doctor to verify.
Next steps
The first hour is behind you. Here is a natural week-one path:
- What you can do with mailglass — the JTBD map; read once straight through when evaluating.
- Authoring mailables — native setter API, HEEx layouts, reusable components.
- Preview — dev preview at
/dev/mail; device-width and dark-mode toggles. - Webhooks — webhook ingest, verification, and suppression wiring.
- Testing — Fake adapter, TestAssertions, and the
deliver/2baseline. - Telemetry and operating —
[:mailglass, :outbound, :dispatch, ...]events and alerting.
For a fuller ordered index, see the learning path.