ecto-patterns — Claude Code skill: Ecto patterns for Phoenix
A skill that feeds your agent Ecto conventions for Phoenix apps: schemas, changesets, migrations, transactions and multi-tenancy.
npx skills add bobmatnyc/claude-mpm-skills --skill ecto-patterns -a claude-codeWhat ecto-patterns does
The skill covers the whole data layer of a Phoenix app: schemas and changesets with validation, migrations, queries with preload, Ecto.Multi, database constraints, pagination, tenant partitioning and tests. It ships as reference material with code samples the agent pulls in when you ask for anything Ecto-related.
The conventions are opinionated in a useful way. Casting and validation stay in changesets, business logic stays in contexts, and every validation gets a matching database constraint (unique_constraint, foreign_key_constraint). Multi-step writes go through Repo.transaction or Ecto.Multi; large tables get indexes with concurrently: true and migrations split into small steps.
There is a pitfalls section too: risky DDL crammed into one migration, querying associations in a loop instead of preloading, missing transactions around multi-step writes. If you run prefix-based multi-tenancy on Postgres, note that each tenant needs its own migrations — worth checking how your deploy handles that.
What it helps with
- write a schema and changeset with field validation
- run a migration without locking a large table
- build a multi-step write with Ecto.Multi
- kill an N+1 when rendering a list
- split tenant data across Postgres prefixes
Example prompts for the agent
- add a User schema and a registration changeset with email validation
- rewrite this multi-step insert using Ecto.Multi
- why is there an N+1 here and how do I fix it with preload
How to install ecto-patterns
1Skills CLI (Vercel)
npx skills add bobmatnyc/claude-mpm-skills --skill ecto-patterns -a claude-code2Manually: put the files in place
~/.claude/skills/ecto-patterns/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/ecto-patterns/
1Skills CLI (Vercel)
npx skills add bobmatnyc/claude-mpm-skills --skill ecto-patterns -a codex2Manually: put the files in place
.agents/skills/ecto-patterns/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add bobmatnyc/claude-mpm-skills --skill ecto-patterns -a cursor2Manually: put the files in place
.cursor/skills/ecto-patterns/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install bobmatnyc/claude-mpm-skills ecto-patterns2Manually: put the files in place
.github/skills/ecto-patterns/SKILL.md1Skills CLI (Vercel)
npx skills add bobmatnyc/claude-mpm-skills --skill ecto-patterns -a gemini-cli2Platform CLI
gemini skills install https://github.com/bobmatnyc/claude-mpm-skills --path toolchains/elixir/data/ecto-patternsManually: put the files in place
.devin/skills/ecto-patterns/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add bobmatnyc/claude-mpm-skills --skill ecto-patterns -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about ecto-patterns
How do I install ecto-patterns in Claude?
Run npx skills add bobmatnyc/claude-mpm-skills --skill ecto-patterns -a claude-code. The skill lands at ~/.claude/skills/ecto-patterns/SKILL.md.
What does ecto-patterns do?
It gives the agent a set of Ecto patterns: schemas, changesets, migrations, queries, transactions, constraints, pagination, multi-tenancy and testing. Think of it as a reference the agent consults on data-layer tasks.
Which platforms can use this skill?
Beyond Claude there are install commands for Codex, Cursor, GitHub Copilot, Gemini CLI and OpenCode. Each platform has its own command and its own SKILL.md path.
Source text
SKILL.md · MIT
---
name: ecto-patterns
description: Ecto patterns for Phoenix/Elixir apps. Covers schemas, changesets, migrations, queries, Ecto.Multi, transactions, constraints, associations, pagination, tenant partitioning, performance, and testing.
user-invocable: false
disable-model-invocation: true
version: 1.0.0
category: toolchain
author: Claude MPM Team
license: MIT
progressive_disclosure:
entry_point:
summary: "Ecto schemas/changesets, migrations, queries, transactions, constraints, pagination, tenancy, and performance for Phoenix apps."
when_to_use:
- "Designing schemas and changesets with validation/constraints"
- "Running migrations safely (DDL ordering, locks) and managing data migrations"
- "Executing transactional flows with Ecto.Multi"
- "Implementing pagination, preloading, and multi-tenant partitioning"
quick_start:
- "mix ecto.create && mix ecto.migrate"
- "Define schemas + changesets; enforce DB constraints (unique/foreign keys)"
- "Use Repo.transaction / Ecto.Multi for multi-step writes"
- "Preload associations; paginate with limit/offset or cursor libs (Scrivener/Flop)"
token_estimate:
entry: 170
full: 5400
---
# Ecto Patterns for Phoenix/Elixir
Ecto is the data layer for Phoenix applications: schemas, changesets, queries, migrations, and transactions. Good Ecto practice keeps domain logic in contexts, enforces constraints in the database, and uses transactions for multi-step workflows.
## Schemas and Changesets
```elixir
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :hashed_password, :string
field :confirmed_at, :naive_datetime
has_many :memberships, MyApp.Orgs.Membership
timestamps()
end
def registration_changeset(user, attrs) do
user
|> cast(attrs, [:email, :password])
|> validate_required([:email, :password])
|> validate_format(:email, ~r/@/)
|> validate_length(:password, min: 12)
|> unique_constraint(:email)
|> hash_password()
end
defp hash_password(%{valid?: true} = cs),
do: put_change(cs, :hashed_password, Argon2.hash_pwd_salt(get_change(cs, :password)))
defp hash_password(cs), do: cs
end
```
**Guidelines**
- Keep casting/validation in changesets; keep business logic in contexts.
- Always pair validation with DB constraints (`unique_constraint`, `foreign_key_constraint`).
- Use `changeset/2` for updates; avoid mass assigning without casting.
## Migrations
```elixir
def change do
create table(:users) do
add :email, :citext, null: false
add :hashed_password, :string, null: false
add :confirmed_at, :naive_datetime
timestamps()
end
create unique_index(:users, [:email])
end
```
**Safe migration tips**
- Prefer additive changes: add columns nullable, backfill, then enforce null: false.
- For large tables: use `concurrently: true` for indexes; disable in `change` and wrap in `up/down` for Postgres.
- Data migrations belong in separate modules called from `mix ecto.migrate` via `execute/1` or in distinct scripts; ensure idempotence.
- Coordinate locks: avoid long transactions; break migrations into small steps.
## Queries and Preloads
```elixir
import Ecto.Query
def list_users(opts \\ %{}) do
base =
from u in MyApp.Accounts.User,
preload: [:memberships],
order_by: [desc: u.inserted_at]
Repo.all(apply_pagination(base, opts))
end
defp apply_pagination(query, %{limit: limit, offset: offset}),
do: query |> limit(^limit) |> offset(^offset)
defp apply_pagination(query, _), do: query
```
**Patterns**
- Use `preload` rather than calling Repo in loops; prefer `Repo.preload/2` after fetching.
- Use `select` to avoid loading large blobs.
- For concurrency, use `Repo.transaction` with `lock: "FOR UPDATE"` in queries that need row-level locks.
## Transactions and Ecto.Multi
```elixir
alias Ecto.Multi
def onboard_user(attrs) do
Multi.new()
|> Multi.insert(:user, User.registration_changeset(%User{}, attrs))
|> Multi.insert(:org, fn %{user: user} ->
Org.changeset(%Org{}, %{owner_id: user.id, name: attrs["org_name"]})
end)
|> Multi.run(:welcome, fn _repo, %{user: user} ->
MyApp.Mailer.deliver_welcome(user)
{:ok, :sent}
end)
|> Repo.transaction()
end
```
**Guidelines**
- Prefer `Multi.run/3` for side effects that can fail; return `{:ok, value}` or `{:error, reason}`.
- Use `Multi.update_all` for batch updates; include `where` guards to prevent unbounded writes.
- Propagate errors upward; translate them in controllers/LiveViews.
## Associations and Constraints
- Use `on_replace: :delete`/`:nilify` to control nested changes.
- Define `foreign_key_constraint/3` and `unique_constraint/3` in changesets to surface DB errors cleanly.
- For many-to-many, prefer join schema (`has_many :memberships`) instead of automatic `many_to_many` when you need metadata.
## Pagination and Filtering
- Offset/limit for small datasets; cursor-based for large lists (`Scrivener`, `Flop`, `Paginator`).
- Normalize filters in contexts; avoid letting controllers build queries directly.
- Add composite indexes to match filter columns; verify with `EXPLAIN ANALYZE`.
## Multi-Tenancy Patterns
- **Prefix-based**: Postgres schemas per tenant (`put_source/2` with `prefix:`) — good isolation, needs per-tenant migrations.
- **Row-based**: `tenant_id` column + row filters — simpler migrations; add partial indexes per tenant when large.
- Always scope queries by tenant in contexts; consider using policies/guards to enforce.
## Performance and Ops
- Use `Repo.stream` for large exports; wrap in `Repo.transaction`.
- Cache hot reads with ETS/Cachex; invalidate on writes.
- Watch query counts in LiveView/Channels; preload before rendering to avoid N+1.
- Telemetry: `OpentelemetryEcto` exports query timings; add DB connection pool metrics.
## Testing
```elixir
use MyApp.DataCase, async: true
test "registration changeset validates email" do
changeset = User.registration_changeset(%User{}, %{email: "bad", password: "secretsecret"})
refute changeset.valid?
assert %{email: ["has invalid format"]} = errors_on(changeset)
end
```
- `DataCase` sets up sandboxed DB; keep tests async unless transactions conflict.
- Use factories/fixtures in `test/support` to build valid structs quickly.
- For migrations, add regression tests for constraints (unique/index-backed constraints).
## Common Pitfalls
- Running risky DDL in a single migration step (avoid locks; break apart).
- Skipping DB constraints and relying only on changesets.
- Querying associations in loops instead of preloading.
- Missing transactions for multi-step writes (partial state on failure).
- Forgetting tenant scoping on read/write in multi-tenant setups.
© Claude MPM Team · MIT · bobmatnyc/claude-mpm-skills