Curated library of 38 atomic skills, 7 personas, and 1 orchestrator for Elixir and Phoenix development. Organized by category: fundamentals, phoenix, database, testing, auth, infrastructure, quality, security, integrations, tooling, frameworks, personas, and orchestration. Covers core Elixir patterns, Phoenix LiveView, Ecto, OTP, Oban, testing, security, deployment, real-time, and modern tooling (Req, Swoosh, Cachex, Broadway, Ash).
91
91%
Does it follow best practices?
Impact
91%
1.37xAverage score across 56 eval scenarios
Advisory
Suggest reviewing before use
MyApp.Emails.UserEmail, not inline in contexts# mix.exs
defp deps do
[
{:swoosh, "~> 1.14"},
{:finch, "~> 0.18"},
{:gen_smtp, "~> 1.0"}
]
end
# application.ex
def start(_type, _args) do
children = [
# ...
{Finch, name: MyApp.Finch}
]
# ...
endAfter adding deps and configuring the supervision tree, confirm everything works before writing email modules:
# In iex -S mix (dev environment with Local adapter)
MyApp.Mailer.deliver(Swoosh.Email.new(to: "test@example.com", from: "noreply@myapp.com", subject: "Test"))
# => {:ok, %{}} — mailer configured correctly
# => {:error, ...} — Finch missing from supervision tree or adapter misconfiguredSee assets/mailer_template.ex for a copy-paste template with the Mailer module, an email-builder module, and an Oban delivery worker.
# lib/my_app/emails/user_email.ex
defmodule MyApp.Emails.UserEmail do
import Swoosh.Email
import Phoenix.Component, only: [sigil_H: 2]
alias MyAppWeb.EmailComponents
def welcome(user) do
assigns = %{user: user}
html =
~H"""
<EmailComponents.layout>
<h1>Welcome, <%= @user.name %>!</h1>
<p>Thanks for signing up.</p>
<EmailComponents.button href={url(~p"/dashboard")}>Get Started</EmailComponents.button>
</EmailComponents.layout>
"""
|> Phoenix.HTML.Safe.to_iodata()
|> IO.iodata_to_binary()
new()
|> to({user.name, user.email})
|> from({"MyApp", "noreply@myapp.com"})
|> subject("Welcome to MyApp!")
|> html_body(html)
|> text_body("Welcome, #{user.name}! Thanks for signing up.")
end
end# lib/my_app_web/components/email_components.ex
defmodule MyAppWeb.EmailComponents do
use Phoenix.Component
def layout(assigns) do
~H"""
<html>
<body style="font-family: sans-serif; max-width: 600px; margin: auto;">
<%= render_slot(@inner_block) %>
</body>
</html>
"""
end
def button(assigns) do
~H"""
<a href={@href} style="background: #4F46E5; color: white; padding: 12px 24px; border-radius: 6px; text-decoration: none;">
<%= render_slot(@inner_block) %>
</a>
"""
end
end# lib/my_app/mailer.ex
defmodule MyApp.Mailer do
use Swoosh.Mailer, otp_app: :my_app
end# config/dev.exs
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.Local
config :swoosh, serve: true
# config/test.exs
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.Test
# config/runtime.exs (production)
config :my_app, MyApp.Mailer,
adapter: Swoosh.Adapters.Sendgrid,
api_key: System.get_env("SENDGRID_API_KEY")# lib/my_app/workers/send_welcome_email.ex
defmodule MyApp.Workers.SendWelcomeEmail do
use Oban.Worker, queue: :mailers, max_attempts: 3
@impl Oban.Worker
def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
user = Accounts.get_user!(user_id)
user
|> MyApp.Emails.UserEmail.welcome()
|> MyApp.Mailer.deliver()
{:ok, :sent}
end
end
# Enqueue from context
def register_user(attrs) do
with {:ok, user} <- create_user(attrs) do
%{user_id: user.id}
|> MyApp.Workers.SendWelcomeEmail.new()
|> Oban.insert()
{:ok, user}
end
enddef register_user(attrs) do
with {:ok, user} <- create_user(attrs) do
Task.start(fn ->
user |> UserEmail.welcome() |> Mailer.deliver()
end)
{:ok, user}
end
enddefmodule MyApp.AccountsTest do
use MyApp.DataCase, async: true
import Swoosh.TestAssertions
test "sends welcome email on registration" do
attrs = %{email: "test@example.com", password: "password123"}
assert {:ok, user} = Accounts.register_user(attrs)
assert_email_sent(fn email ->
assert email.to == [{user.name, user.email}]
assert email.subject =~ "Welcome"
end)
end
test "no email sent on failed registration" do
attrs = %{email: "", password: ""}
assert {:error, _changeset} = Accounts.register_user(attrs)
assert_no_email_sent()
end
end# config/dev.exs
config :swoosh, serve: true
# Access preview at http://localhost:4000/dev/mailbox| ❌ Don't | ✅ Do |
|---|---|
| Build emails inline inside a context | Define them in dedicated modules (MyApp.Emails.UserEmail) |
Call Mailer.deliver/1 inside a web request | Enqueue an Oban job (or Task.start for simple cases) so the request never blocks on SMTP |
| Ship an HTML-only email | Always set both html_body/2 and text_body/2 |
| Use a real adapter in dev/test | Swoosh.Adapters.Local in dev, Swoosh.Adapters.Test in test, real adapter only in prod |
| Hardcode the API key in config | Read it at runtime: System.get_env("SENDGRID_API_KEY") in runtime.exs |
| Assert delivery by inspecting logs | Use Swoosh.TestAssertions — assert_email_sent/1 and assert_no_email_sent/0 |
Forget {Finch, name: MyApp.Finch} in the supervision tree | Start Finch so API-based adapters have an HTTP client |
| Predecessor | This Skill | Successor |
|---|---|---|
| phoenix-auth-customization | swoosh-emails | oban-essentials |
| ecto-essentials | swoosh-emails | testing-essentials |
Companion skills:
oban-essentials — deliver emails asynchronously with retriestesting-essentials — assert delivery with Swoosh.TestAssertions.tessl-plugin
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
scenario-44
scenario-45
scenario-46
scenario-47
scenario-48
scenario-49
scenario-50
scenario-51
scenario-52
scenario-53
scenario-54
scenario-55
scenario-56
skills
frameworks
ash-framework
infrastructure
orchestration
elixir-skill-router
personas
phoenix
quality
security
security-essentials
tooling
mix-tasks-generators