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
Use this skill before modifying ANY schema, query, or migration.
^Define schemas with proper types and associations. The example below shows a child schema with belongs_to; a parent schema uses has_many in the same pattern (see Folder in the migration section for reference).
defmodule MyApp.Media.Image do
use Ecto.Schema
import Ecto.Changeset
schema "images" do
field :title, :string
field :filename, :string
field :content_type, :string
belongs_to :folder, MyApp.Media.Folder # parent uses has_many :images, MyApp.Media.Image
timestamps()
end
def changeset(image, attrs) do
image
|> cast(attrs, [:title, :filename, :content_type, :folder_id])
|> validate_required([:title, :filename, :content_type])
|> validate_length(:title, min: 1, max: 255)
|> validate_inclusion(:content_type, ["image/jpeg", "image/png", "image/gif"])
|> foreign_key_constraint(:folder_id)
end
endimport Ecto.Query
def list_images_by_folder(folder_id) do
Image
|> where([i], i.folder_id == ^folder_id)
|> order_by([i], desc: i.inserted_at)
|> Repo.all()
end
def search_images(query_string) do
search = "%#{query_string}%"
Image
|> where([i], ilike(i.title, ^search))
|> Repo.all()
end❌ Bad — N+1 queries:
images = Repo.all(Image)
Enum.each(images, fn image -> image.folder.name end)✅ Good — single query with preload:
images =
Image
|> preload(:folder)
|> Repo.all()
Enum.each(images, fn image -> image.folder.name end)def transfer_images(image_ids, from_folder_id, to_folder_id) do
Repo.transaction(fn ->
with {:ok, from_folder} <- get_folder(from_folder_id),
{:ok, to_folder} <- get_folder(to_folder_id),
{count, nil} <- update_images(image_ids, to_folder_id) do
{:ok, count}
else
{:error, reason} -> Repo.rollback(reason)
_ -> Repo.rollback(:unknown_error)
end
end)
enddef create_user_with_profile(user_attrs, profile_attrs) do
Ecto.Multi.new()
|> Ecto.Multi.insert(:user, User.changeset(%User{}, user_attrs))
|> Ecto.Multi.insert(:profile, fn %{user: user} ->
Profile.changeset(%Profile{}, Map.put(profile_attrs, :user_id, user.id))
end)
|> Repo.transaction()
endOn failure, the error tuple identifies the named step: {:error, :user, changeset, _changes} or {:error, :profile, changeset, _changes}.
def add_image_to_folder(folder, image_attrs) do
folder
|> Ecto.build_assoc(:images)
|> Image.changeset(image_attrs)
|> Repo.insert()
enddef create_or_update_folder(attrs) do
%Folder{}
|> Folder.changeset(attrs)
|> Repo.insert(
on_conflict: {:replace, [:name, :updated_at]},
conflict_target: :name
)
enddef list_images(filters) do
Enum.reduce(filters, Image, fn
{:folder_id, id}, q -> where(q, [i], i.folder_id == ^id)
{:search, term}, q -> where(q, [i], ilike(i.title, ^"%#{term}%"))
{:content_type, ct}, q -> where(q, [i], i.content_type == ^ct)
_, q -> q
end)
|> Repo.all()
endWrite clear, reversible migrations. After writing a migration, always validate it with the steps below.
defmodule MyApp.Repo.Migrations.CreateImages do
use Ecto.Migration
def change do
create table(:images) do
add :title, :string, null: false
add :filename, :string, null: false
add :content_type, :string, null: false
add :folder_id, references(:folders, on_delete: :nilify_all)
timestamps()
end
create index(:images, [:folder_id])
create index(:images, [:inserted_at])
end
endMigration validation workflow:
mix ecto.migrate — confirm it applies without errorsmix ecto.rollback — confirm it reverses cleanlymix ecto.migrate again — confirm re-applying succeedsAdd unique constraints in migration AND schema changeset (already shown in the Folder schema above).
# Migration
create unique_index(:folders, [:name])Never call Repo from the web layer (LiveViews, controllers) — all database operations belong in context modules.
defmodule MyApp.Media do
alias MyApp.Media.{Image, Folder}
alias MyApp.Repo
def create_image(attrs) do
%Image{}
|> Image.changeset(attrs)
|> Repo.insert()
end
endAll standard CRUD functions (list_*, get_*!, update_*, delete_*) follow the same pattern.
mix ecto.schema directly instead)ecto-changeset-patterns for Ecto.Multi and nested association patternsecto-migration persona instead for migration orchestration