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
Use this skill when writing or reviewing Ecto database code to ensure consistent, idiomatic patterns.
Precondition: Invoke ecto-essentials before this skill for the full Ecto reference.
Repo.get/1, Repo.insert/1) — use bang only in tests^ for interpolation, never string concatenation in fragmentforeign_key_constraint and unique_constraint in changesets to match database constraintsWhen reviewing existing Ecto code, follow these steps in order:
lib/ for Repo. in LiveViews, controllers, or non-context modules! function in application lib/ as a potential bugfor, Enum.map, etc. without prior preloadingRepo calls without Ecto.Multi is a missing transaction❌ Bad — Repo called directly in LiveView:
def handle_event("load", _params, socket) do
users = MyApp.Repo.all(User)
{:noreply, assign(socket, :users, users)}
end✅ Good — LiveView delegates to context:
def handle_event("load", _params, socket) do
users = Accounts.list_users()
{:noreply, assign(socket, :users, users)}
endThe context module owns Repo:
defmodule MyApp.Accounts do
alias MyApp.Repo
alias MyApp.Accounts.User
def list_users, do: Repo.all(User)
def get_user(id), do: Repo.get(User, id)
end❌ Bad — bang in application logic:
def show(conn, %{"id" => id}) do
user = Repo.get!(User, id)
render(conn, :show, user: user)
end✅ Good — non-bang with pattern matching:
def show(conn, %{"id" => id}) do
case Accounts.get_user(id) do
{:ok, user} -> render(conn, :show, user: user)
{:error, :not_found} -> put_status(conn, :not_found) |> json(%{error: "Not found"})
end
endThe context returns tagged tuples:
def get_user(id) do
case Repo.get(User, id) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
endCheckpoint: Search for ! functions in application lib/ — every one is a potential bug.
❌ Bad — missing database constraints, no validation:
def create_user(attrs) do
%User{}
|> Repo.insert(attrs)
end✅ Good — changeset validates and enforces constraints:
def create_user(attrs) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
def changeset(user, attrs) do
user
|> cast(attrs, [:email, :name])
|> validate_required([:email, :name])
|> validate_length(:name, min: 1, max: 255)
|> validate_format(:email, ~r/@/)
|> unique_constraint(:email)
|> foreign_key_constraint(:organization_id)
end❌ Bad — N+1 queries inside loop:
users = Repo.all(User)
for user <- users do
user.posts
end✅ Good — preload before iteration:
users = Repo.all(User) |> Repo.preload(:posts)
for user <- users do
user.posts
end✅ Good — nested preloading:
Repo.all(from u in User, preload: [posts: :comments])Checkpoint: Run Ecto query log observer in development to detect N+1 violations.
❌ Bad — multiple Repo calls without transaction:
def create_user_with_profile(attrs) do
{:ok, user} = Repo.insert(User.changeset(%User{}, attrs))
{:ok, profile} = Repo.insert(Profile.changeset(%Profile{}, Map.put(attrs, :user_id, user.id)))
{:ok, %{user: user, profile: profile}}
end✅ Good — Ecto.Multi wraps all operations in a transaction:
def 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 {:error, :user, changeset, _} names the failed step for targeted error handling.
❌ Bad — string interpolation in fragment:
from(u in User, where: fragment("lower(#{field}) = ?", ^value))✅ Good — parameterized queries with ^:
from(u in User, where: fragment("lower(?) = ?", field(u, :status), ^value))
from(u in User, where: u.status == ^status and u.name == ^name)✅ Good — Enum.reduce for dynamic where clauses:
def list_users(filters) do
Enum.reduce(filters, User, fn
{:status, status}, q -> where(q, status: ^status)
{:search, term}, q -> where(q, ilike: [name: ^"%#{term}%"])
_, q -> q
end)
|> Repo.all()
end❌ Bad — irreversible migration, no index:
def up do
alter table(:users) do
remove :name
end
end✅ Good — reversible change/0 with indexes:
def change do
create table(:images) do
add :title, :string, null: false
add :filename, :string, null: false
add :folder_id, references(:folders, on_delete: :nilify_all)
timestamps()
end
create index(:images, [:folder_id])
create index(:images, [:inserted_at])
endMigration verification steps:
mix ecto.migrate in the test environment first to catch errors earlymix ecto.rollback — confirm the migration rolls back cleanly❌ Bad — no pagination on large tables:
def list_posts, do: Repo.all(Post)✅ Good — offset/limit pagination with composite index:
def list_posts(page \\ 1, per_page \\ 20) do
offset = (page - 1) * per_page
Post
|> order_by(desc: :inserted_at)
|> offset(^offset)
|> limit(^per_page)
|> Repo.all()
end| ❌ Don't | ✅ Do |
|---|---|
Call Repo directly from a LiveView or controller | Put every query behind a context module function |
Use bang functions (Repo.get!) in application logic | Use non-bang functions and pattern match on {:ok, _} / nil |
Interpolate user input into a fragment string | Parameterize with ^ and field(u, :col) |
| Access associations inside a loop without preloading | Repo.preload/2 (or preload:) before iterating |
Chain 2+ related Repo writes without a transaction | Wrap them in Ecto.Multi and Repo.transaction/1 |
| Mix schema changes and data backfill in one migration | Keep structural and data migrations separate |
| Ship foreign keys without indexes | create index(...) on FKs and frequently queried columns |
| Predecessor | This Skill | Successor |
|---|---|---|
| ecto-essentials | apply-ecto-conventions | code-quality |
| ecto-changeset-patterns | apply-ecto-conventions | testing-essentials |
Companion skills:
ecto-essentials — full Ecto reference (schemas, queries, migrations)ecto-changeset-patterns — advanced validations and changeset compositionecto-nested-associations — cast_assoc, Ecto.Multi, cascade operations.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