Configure Postmark as the production email adapter for Swoosh in a Phoenix 1.8 app.
What Gets Configured
- prod.exs — Swoosh API client set to
Swoosh.ApiClient.Req, local storage disabled - runtime.exs — Postmark adapter with
POSTMARK_API_KEYenv var, configurable from address withMAILER_FROM_NAMEandMAILER_FROM_EMAIL - config.exs — Default
Swoosh.Adapters.Localfor dev, default:mailer_fromtuple - test.exs —
Swoosh.Adapters.Testadapter, API client disabled - .env.sample —
POSTMARK_API_KEY,MAILER_FROM_EMAIL,MAILER_FROM_NAMEentries - Mailer module — Verified or created
App Name Detection
Detect from mix.exs:
- App module prefix (e.g.,
MyApp) — the module namespace - OTP app name (e.g.,
:my_app) — used in config atoms - Web module prefix (e.g.,
MyAppWeb) — for endpoint references
Also detect:
- Mailer module — Look for
use Swoosh.Mailerinlib/ - Existing Swoosh config — Check all config files for existing adapter settings
Phase 0: Discovery
CRITICAL: Audit the target app before making any changes.
Discovery Checklist
-
Swoosh dependency — Check
mix.exsfor{:swoosh, ...}- If missing: add
{:swoosh, "~> 1.16"}to deps and tell user to runmix deps.get
- If missing: add
-
Req dependency — Check
mix.exsfor{:req, ...}- Swoosh.ApiClient.Req requires Req. If missing: add
{:req, "~> 0.5"}to deps - If the app uses
Swoosh.ApiClient.FinchorSwoosh.ApiClient.Hackneyinstead, prefer Req but note the existing choice
- Swoosh.ApiClient.Req requires Req. If missing: add
-
Mailer module — Search for
use Swoosh.Mailerinlib/- If found: note the module name and otp_app
- If missing: will create one
-
Existing config — Check each config file:
config/config.exs— look for existing mailer adapter configconfig/dev.exs— look for swoosh settingsconfig/test.exs— look for swoosh settingsconfig/prod.exs— look for swoosh settingsconfig/runtime.exs— look for mailer config inif config_env() == :prodblock
-
Existing .env.sample — Check for
POSTMARK_API_KEYorMAILER_FROMentries -
Existing from address config — Search for
:mailer_fromin config files
Discovery Report
Present findings to user. If Swoosh is already fully configured for Postmark, confirm and skip. Otherwise show what will be added/changed.
Phase 1: Implementation
Execute in order, skipping anything that already exists.
Step 1: Dependencies
Ensure mix.exs has:
{:swoosh, "~> 1.16"}
{:req, "~> 0.5"}
Step 2: Mailer Module
If no mailer module exists, create lib/<app>/mailer.ex:
defmodule <AppModule>.Mailer do
@moduledoc """
Email delivery for the application using Swoosh.
"""
use Swoosh.Mailer, otp_app: :<otp_app>
end
Step 3: Base Config (config/config.exs)
Add if not present — sets Local adapter for dev and a default from address:
# Mailer — uses Local adapter by default (see runtime.exs for production)
config :<otp_app>, <AppModule>.Mailer, adapter: Swoosh.Adapters.Local
config :<otp_app>, :mailer_from, {"<AppName>", "contact@example.com"}
Step 4: Dev Config (config/dev.exs)
Add if not present:
# Disable swoosh api client as it is only required for production adapters.
config :swoosh, :api_client, false
Step 5: Test Config (config/test.exs)
Add if not present:
config :<otp_app>, <AppModule>.Mailer, adapter: Swoosh.Adapters.Test
# Disable swoosh api client as it is only required for production adapters
config :swoosh, :api_client, false
Step 6: Prod Config (config/prod.exs)
Add if not present:
# Configure Swoosh API Client
config :swoosh, api_client: Swoosh.ApiClient.Req
# Disable Swoosh Local Memory Storage
config :swoosh, local: false
Step 7: Runtime Config (config/runtime.exs)
Inside the if config_env() == :prod do block, add:
# Postmark mailer adapter
config :<otp_app>, <AppModule>.Mailer,
adapter: Swoosh.Adapters.Postmark,
api_key:
System.get_env("POSTMARK_API_KEY") ||
raise("environment variable POSTMARK_API_KEY is missing.")
# Mailer from address
config :<otp_app>,
:mailer_from,
{System.get_env("MAILER_FROM_NAME", "<AppName>"),
System.get_env("MAILER_FROM_EMAIL") ||
raise("environment variable MAILER_FROM_EMAIL is missing.")}
Important: Place this inside the existing if config_env() == :prod do block — do not create a duplicate conditional.
Step 8: Environment File (.env.sample)
Add these entries if not already present:
# Postmark mailer adapter
POSTMARK_API_KEY=
# Required in production (sender email for system mail)
MAILER_FROM_EMAIL=noreply@example.com
# Optional (defaults to <AppName>)
MAILER_FROM_NAME=<AppName>
Step 9: Dev Mailbox Route (Optional)
If the router doesn't already have the Swoosh dev mailbox route, suggest adding to router.ex inside a dev-only scope:
if Mix.env() == :dev do
scope "/dev" do
pipe_through :browser
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end
Only suggest this — don't add it automatically since it touches routing.
Phase 2: Verification
- Run
mix compile --warnings-as-errorsto verify config compiles - Check that
mix teststill passes - Tell the user to:
- Set
POSTMARK_API_KEYin their production environment - Set
MAILER_FROM_EMAILto a verified sender domain in Postmark - Optionally set
MAILER_FROM_NAME
- Set
Usage Pattern
The app should use the :mailer_from config when sending emails:
{from_name, from_email} = Application.get_env(:<otp_app>, :mailer_from)
Swoosh.Email.new()
|> Swoosh.Email.from({from_name, from_email})
|> Swoosh.Email.to(recipient)
|> Swoosh.Email.subject("Hello")
|> Swoosh.Email.text_body("...")
|> <AppModule>.Mailer.deliver()
Notes
- Postmark requires a verified sender signature or domain before emails will deliver
- The
Swoosh.ApiClient.Reqclient is preferred over Finch/Hackney for new Phoenix apps - Local adapter in dev provides the
/dev/mailboxpreview UI - Test adapter captures emails for assertion in ExUnit tests via
Swoosh.TestAssertions