code-quality
Use when refactoring or reviewing Elixir code quality — compiler warnings and Credo first, then the plugin's on-demand…
Use when adding fields to the phx.gen.auth user — usernames, profile fields, a registration changeset, migrations and fixtures alongside generated auth. Not for on_mount/live_session (phoenix-liveview-auth) or permissions (phoenix-authorization-patterns).
$ npx -y skills add j-morgan6/elixir-phoenix-guide --skill phoenix-auth-customization --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/phoenix-auth-customizationContext preview
The summary Claude sees to decide when to auto-load this skill.
Use when adding fields to the phx.gen.auth user — usernames, profile fields, a registration changeset, migrations and fixtures alongside generated auth. Not for on_mount/live_session (phoenix-liveview-auth) or permissions (phoenix-authorization-patterns).
name: phoenix-auth-customization description: "Use when adding fields to the phx.gen.auth user — usernames, profile fields, a registration changeset, migrations and fixtures alongside generated auth. Not for on_mount/live_session (phoenix-liveview-auth) or permissions (phoenix-authorization-patterns)."
> Verified against Elixir 1.20, Phoenix 1.8, LiveView 1.2, Ecto 3.14 — October 2026
1. **Use the generated auth; extend it, don't rewrite it** — keep `UserAuth`, `UserToken`, the session controller and the generated tests. Add fields and functions next to them; never paste a pre-1.8 `User` module or `UserAuth` over the generated one. 2. **Never edit the generated auth migration** — add a new migration for each new column. 3. **Add a `registration_changeset/3` that pipes `email_changeset/3`** — 1.8 has no registration changeset: `register_user/1` uses `email_changeset/3`, which the settings page also uses to change email. Requiring a new field inside `email_changeset` breaks email changes. 4. **Registration is email-only (magic link)** — don't add a password input to registration. A password set at sign-up is silently dropped by `email_changeset`, and if you make it stick, `login_user_by_magic_link/1` raises for unconfirmed users with a password (see "Mixing magic link and password registration" in `mix help phx.gen.auth`). 5. **Add only the new line to the schema** — keep `field :authenticated_at, :utc_datetime, virtual: true` (sudo mode reads it) and `timestamps(type: :utc_datetime)`. 6. **Use the generated fixtures; add the new field to `valid_user_attributes/1`** — `user_fixture/1` already confirms the user via a magic link; `unconfirmed_user_fixture/1`, `user_scope_fixture/0,1` and `set_password/1` already exist. 7. **Never `cast` privileged fields** — `:role`, `:confirmed_at`, `:org_id`, `:hashed_password` are set with `change/2` / `put_change/3` in a dedicated context function, never from form params. 8. **Uniqueness = unique index + `unique_constraint`** — plus `unsafe_validate_unique` for early feedback, skipped during `phx-change` via the generator's `validate_unique: false` option. 9. **Update every caller of the changed changeset** — the registration LiveView's `mount` and `"validate"` handler, `register_user/1`, and the generated `accounts_test.exs` tests that call `register_user(%{email: ...})` directly.
---
mix phx.gen.auth Accounts User users --live # or --no-live for controllers
Users register with an email only, receive a magic link, and may set a password later in settings.
---
mix ecto.gen.migration add_username_to_users
defmodule MyApp.Repo.Migrations.AddUsernameToUsers do
use Ecto.Migration
def change do
alter table(:users) do
# citext (enabled by the auth migration) makes uniqueness case-insensitive, like email
add :username, :citext, null: false
end
create unique_index(:users, [:username])
end
endOn a table that already has rows, add the column nullable, backfill, then add `NOT NULL` and build the unique index with `concurrently: true` in later migrations (the steps are in the `ecto-migrations-safety` skill).
schema "users" do # ...generated fields stay as they are field :username, :string end
@doc """
A user changeset for registration. Same options as `email_changeset/3`.
"""
def registration_changeset(user, attrs, opts \\ []) do
user
|> email_changeset(attrs, opts)
|> cast(attrs, [:username])
|> validate_username(opts)
end
defp validate_username(changeset, opts) do
changeset =
changeset
|> validate_required([:username])
|> validate_length(:username, min: 3, max: 30)
|> validate_format(:username, ~r/^[a-zA-Z0-9_]+$/,
message: "only letters, numbers, and underscores"
)
if Keyword.get(opts, :validate_unique, true) do
changeset
|> unsafe_validate_unique(:username, MyApp.Repo)
|> unique_constraint(:username)
else
changeset
end
end# accounts.ex
def register_user(attrs) do
%User{}
|> User.registration_changeset(attrs)
|> Repo.insert()
end
def change_user_registration(%User{} = user, attrs \\ %{}, opts \\ []) do
User.registration_changeset(user, attrs, opts)
endSwap `change_user_email` for `change_user_registration` in `mount/3` and the `"validate"` handler, and add the input. The `"save"` handler already passes all `user_params` to `register_user/1`.
def mount(_params, _session, socket) do
changeset = Accounts.change_user_registration(%User{}, %{}, validate_unique: false)
{:ok, assign_form(socket, changeset), temporary_assigns: [form: nil]}
end
def handle_event("validate", %{"user" => user_params}, socket) do
changeset = Accounts.change_user_registration(%User{}, user_params, validate_unique: false)
{:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
endKeep the generated first `mount/3` clause that redirects already-logged-in users, and keep `@impl true` on the first clause.
<.form for={@form} id="registration_form"· 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 defining schemas, writing queries, or creating migrations — scoped context functions, Repo.transact, upserts, indexes,…
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…