Skip to content
Development
Skill

/phoenix-auth-customization

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).

BOOST
From plugin
elixir-phoenix-guide
16924 skills3 hooks
Install
$ npx -y skills add j-morgan6/elixir-phoenix-guide --skill phoenix-auth-customization --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/phoenix-auth-customization

Context 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).

SKILL.md

phoenix-auth-customization.SKILL.md
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)."

Phoenix Auth Customization

> Verified against Elixir 1.20, Phoenix 1.8, LiveView 1.2, Ecto 3.14 — October 2026

RULES — Follow these with no exceptions

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.

---

What phx.gen.auth Generates (1.8)

mix phx.gen.auth Accounts User users --live      # or --no-live for controllers
  • `lib/my_app/accounts.ex`, `accounts/user.ex`, `accounts/user_token.ex`, `accounts/user_notifier.ex`, `accounts/scope.ex`
  • `lib/my_app_web/user_auth.ex`, `controllers/user_session_controller.ex`
  • `--live`: `live/user_live/registration.ex`, `login.ex`, `confirmation.ex`, `settings.ex`; `--no-live`: registration, session and settings controllers + templates
  • `test/support/fixtures/accounts_fixtures.ex` and tests for all of the above
  • a `config :my_app, :scopes` entry so later `phx.gen.live`/`phx.gen.context` runs generate scoped code (see the `phoenix-scopes-and-generators` skill)

Users register with an email only, receive a magic link, and may set a password later in settings.

---

Adding a Field (username)

1. Migration

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
end

On 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).

2. Schema — add one line

schema "users" do
  # ...generated fields stay as they are
  field :username, :string
end

3. A registration changeset that builds on email_changeset

@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

4. Point the context at it

# 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)
end

5. Registration LiveView

Swap `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))}
end

Keep 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"
Read more
Ships withelixir-phoenix-guide

· Changelog A Claude Code plugin (with Codex support for the skills) that keeps Claude's Elixir and Phoenix code idiomatic and current.

Get the whole plugin

Other skills on elixir-phoenix-guide.