code-quality
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when defining schemas, writing queries, or creating migrations — scoped context functions, Repo.transact, upserts, indexes, query composition, safe field casting. Not for multi-changeset design (ecto-changeset-patterns) or nested children (ecto-nested-associations).
$ npx -y skills add j-morgan6/elixir-phoenix-guide --skill ecto-essentials --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/ecto-essentialsContext preview
The summary Claude sees to decide when to auto-load this skill.
Use when defining schemas, writing queries, or creating migrations — scoped context functions, Repo.transact, upserts, indexes, query composition, safe field casting. Not for multi-changeset design (ecto-changeset-patterns) or nested children (ecto-nested-associations).
name: ecto-essentials description: "Use when defining schemas, writing queries, or creating migrations — scoped context functions, Repo.transact, upserts, indexes, query composition, safe field casting. Not for multi-changeset design (ecto-changeset-patterns) or nested children (ecto-nested-associations)."
> Verified against Elixir 1.20, Phoenix 1.8, Ecto 3.14 — October 2026
1. **Scope every context function** — take `%Scope{}` first and filter by owner (`Repo.get_by!(Post, id: id, user_id: scope.user.id)`), as the Phoenix 1.8 generators do; never load, update or delete by a client-supplied id alone 2. **Never `cast` foreign keys or privileged fields** (`:user_id`, `:folder_id`, `:role`, `:org_id`, `:confirmed_at`, `:deleted_at`) — set them with `put_change`/`Ecto.build_assoc` after a scope-checked lookup; use the 1.8 `changeset(struct, attrs, scope)` shape 3. **Always use changesets** for inserts and updates — never pass raw maps to Repo 4. **Use `Repo.transact/2` with `with`** for multi-step writes; `Repo.transaction/2` is soft-deprecated since Ecto 3.13. Reach for `Ecto.Multi` only when the steps are dynamic (see the `ecto-nested-associations` skill) 5. **Preload associations** before accessing them — avoid N+1 queries 6. **Add database constraints AND changeset constraint functions** — `unique_index` + `unique_constraint`, `references` + `foreign_key_constraint` 7. **Index every foreign key** and every column you filter or sort on 8. **Use `timestamps(type: :utc_datetime)`** in schemas and migrations — matches Phoenix 1.8's `timestamp_type: :utc_datetime` 9. **Escape `%`, `_` and `\` in user search terms** before building a `like`/`ilike` pattern 10. **Keep `change/0` migrations reversible** — `remove/3` with the type, `modify` with `from:`, `execute/2` with an up and a down 11. **Database access goes through contexts** — never call `Repo` from controllers or LiveViews
---
defmodule MyApp.Media.Image do
use Ecto.Schema
import Ecto.Changeset
schema "images" do
field :title, :string
# Server-generated storage name (UUID + allowlisted extension)
field :filename, :string
# The client's file name, for display only
field :original_name, :string
# Detected from the file's bytes, not the client's claim
field :content_type, :string
field :byte_size, :integer
belongs_to :folder, MyApp.Media.Folder
belongs_to :user, MyApp.Accounts.User
timestamps(type: :utc_datetime)
end
@doc false
def changeset(image, attrs, scope) do
image
|> cast(attrs, [:title, :original_name])
# filename/content_type/byte_size are set on the struct by the upload code
# (see the phoenix-uploads skill); validate_required also checks struct data.
|> validate_required([:title, :filename, :content_type, :byte_size])
|> validate_length(:title, max: 255)
|> put_change(:user_id, scope.user.id)
end
endThis `Image` is the running example in the `phoenix-uploads`, `phoenix-liveview-essentials` (checklist) and `testing-essentials` (reference) skills too.
**Mass assignment:** anything in the `cast` list can be set from a form or JSON body (bad/good pair in the `ecto-changeset-patterns` skill). Set ownership from the scope and foreign keys from a record you loaded through the scope:
def move_image(%Scope{} = scope, %Image{} = image, folder_id) do
true = image.user_id == scope.user.id
folder = get_folder!(scope, folder_id)
image
|> Ecto.Changeset.change(folder_id: folder.id)
|> Ecto.Changeset.foreign_key_constraint(:folder_id)
|> Repo.update()
endTo generate this shape, or an org-scoped variant with `--scope org`, see the `phoenix-scopes-and-generators` skill.
Mirror `mix phx.gen.context` in 1.8: scope first, owner filter on every read, ownership assert on every write. (The generator also broadcasts `{:created | :updated | :deleted, image}` after each write; see the `phoenix-pubsub-patterns` skill.)
defmodule MyApp.Media do
import Ecto.Query, warn: false
alias MyApp.Accounts.Scope
alias MyApp.Media.{Folder, Image}
alias MyApp.Repo
# filters: [folder_id: id, search: term, min_size: bytes] (see Query Composition)
def list_images(%Scope{} = scope, filters \\ []) do
Image
|> where(user_id: ^scope.user.id)
|> filter_images(filters)
|> order_by(desc: :inserted_at, desc: :id)
|> Repo.all()
end
def get_image!(%Scope{} = scope, id) do
Repo.get_by!(Image, id: id, user_id: scope.user.id)
end
def get_folder!(%Scope{} = scope, id) do
Repo.get_by!(Folder, id: id, user_id: scope.user.id)
end
# tmp_path is the upload's temp file; store_file/1 checks its magic bytes,
# names it on the server and copies it. Storage fields never go through cast.
def create_image(%Scope{} = scope, tmp_path, attrs) do
with {:ok, stored} <- store_file(tmp_path) do
Image
|> struct!(stored)
|> Image.changeset(attrs, scope)
|> Repo.insert()
end
end
def update_image(%Scope{} = scope, %Image{} = image, attrs) do
true = image.user_id == scope.user.id
image
|> Image.changeset(attrs, scope)
|> Repo.update()
end
# For uploaded files, the phoenix-uploads version also removes the stored file.
def delete_image(%Scope{} = scope, %Image{} = image) do
true = image.user_id == scope.user.id
Repo.delete(image)
end
# Private helpers not shown: filter_images/2 (Query Composition below) and
# store_file/1 (the phoenix-uploads skill).
end<!-- epg:bad -->
# IDOR: any logged-in user can read or delete any image by guessing ids def get_image!(id), do: Repo.get!(Image, id)
LiveViews and controllers pass `socket.assigns.current_scope` / `conn.assigns.current_scope`.
def search_images(%Scope{} = scope, term) do
pattern =· Changelog A Claude Code plugin (with Codex support for the skills) that keeps Claude's Elixir and Phoenix code idiomatic and current.
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when preparing releases or deployment config — mix phx.gen.release, runtime.exs vs compile-time config, release migrations,…
Use when writing Ecto changeset validations beyond cast/validate_required — custom and cross-field validations (e.g. end date…
Use when migrating PostgreSQL tables that serve live traffic — index concurrently,…
Use when one form or operation manages parent and child records together — cast_assoc/cast_embed, inputs_for with sort/drop…
Use when a query is slow, N+1 or hard to compose — preload vs join-preload, dynamic/2, subquery/exists, Repo.aggregate,…