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).
73
91%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
registration_changeset, email_changeset, etc.)cast_assoc child changesetsunsafe_validate_unique with unique_constraintupdate_change/3 for field transformations (trim, downcase, slugify)opts \\ [] for conditional validationWhen adding changesets to a new schema, apply patterns in this order:
unsafe_validate_unique with unique_constraintupdate_change/3 in the changeset, not in controllersiex -S mix or running your changeset tests. A quick iex smoke test:# In iex -S mix
MyApp.Accounts.User.registration_changeset(%MyApp.Accounts.User{}, %{email: "bad", username: ""})
# => Inspect .valid? and .errors to confirm validations fire as expected
# Or a minimal ExUnit test
test "registration_changeset requires email and username" do
changeset = User.registration_changeset(%User{}, %{})
assert %{email: ["can't be blank"], username: ["can't be blank"]} = errors_on(changeset)
enddefmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :username, :string
field :password, :string, virtual: true, redact: true
field :hashed_password, :string, redact: true
field :bio, :string
timestamps()
end
# Registration — all fields, password hashing
def registration_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:email, :username, :password])
|> validate_email(opts)
|> validate_username()
|> validate_password(opts)
end
# Email change — only email
def email_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:email])
|> validate_email(opts)
end
# Password change — only password
def password_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:password])
|> validate_password(opts)
|> put_password_hash()
end
# Profile update — non-sensitive fields only
def profile_changeset(user, attrs) do
user
|> cast(attrs, [:username, :bio])
|> validate_username()
end
end❌ Bad — :post_id is required but set automatically by cast_assoc:
def changeset(ingredient, attrs) do
ingredient
|> cast(attrs, [:name, :quantity, :post_id])
|> validate_required([:name, :post_id]) # Fails!
end✅ Good — only require user-provided fields:
def changeset(ingredient, attrs) do
ingredient
|> cast(attrs, [:name, :quantity])
|> validate_required([:name])
enddefp validate_email(changeset, opts) do
changeset
|> validate_required([:email])
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
|> validate_length(:email, max: 160)
|> maybe_validate_unique_email(opts)
end
defp validate_username(changeset) do
changeset
|> validate_required([:username])
|> validate_format(:username, ~r/^[a-zA-Z0-9_]+$/, message: "only letters, numbers, and underscores")
|> validate_length(:username, min: 3, max: 30)
|> unsafe_validate_unique(:username, MyApp.Repo)
|> unique_constraint(:username)
end# Normal registration
def register_user(attrs) do
%User{}
|> User.registration_changeset(attrs)
|> Repo.insert()
end
# In tests — skip hashing for speed
def register_user_for_test(attrs) do
%User{}
|> User.registration_changeset(attrs, hash_password: false, validate_email: false)
|> Repo.insert()
enddef changeset(user, attrs) do
user
|> cast(attrs, [:email, :username])
|> update_change(:email, &String.downcase/1)
|> update_change(:username, &String.trim/1)
|> update_change(:username, &String.downcase/1)
end
# For slugs
defp generate_slug(changeset) do
case get_change(changeset, :title) do
nil -> changeset
title ->
slug = title |> String.downcase() |> String.replace(~r/[^a-z0-9]+/, "-") |> String.trim("-")
put_change(changeset, :slug, slug)
end
endAlways pair unsafe_validate_unique with unique_constraint:
def changeset(user, attrs) do
user
|> cast(attrs, [:email, :username])
# Fast check — queries DB, gives immediate UI feedback
|> unsafe_validate_unique(:email, MyApp.Repo)
|> unsafe_validate_unique(:username, MyApp.Repo)
# Constraint check — catches race conditions at insert time
|> unique_constraint(:email)
|> unique_constraint(:username)
end| Skill | When to Use |
|---|---|
ecto-essentials | Start here for schema definitions and migration patterns before writing changesets |
ecto-nested-associations | Use when cast_assoc involves deeply nested data structures |
testing-essentials | Use after this skill to write changeset tests with errors_on/1 helpers |
Each skill can be used independently if companion files are not present.