From elixir-phoenix-guide
Guides extending Phoenix phx.gen.auth with custom user fields like usernames via separate migrations, schema updates, registration changesets, forms, tests, and unique constraints.
How this skill is triggered — by the user, by Claude, or both
Slash command
/elixir-phoenix-guide:phoenix-auth-customizationThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
1. **Never modify generated auth migrations** — create separate migrations for custom fields; generated migrations are tested and correct
registration_changeset to cast and validate new fields — don't create a separate changeset for initial registrationconfirmed_at: DateTime.utc_now(:second) or tests requiring authenticated users will failsave/2 handler — the form must send the field, and the handler must pass it to the contextunique_constraint + database unique index for uniqueness — never validate uniqueness in application code aloneStart with the generator, then extend. Never hand-roll auth.
# Generate auth with LiveView (recommended)
mix phx.gen.auth Accounts User users
# This creates:
# - Migration: priv/repo/migrations/*_create_users_auth_tables.exs
# - Schema: lib/my_app/accounts/user.ex
# - Context: lib/my_app/accounts.ex
# - LiveViews: lib/my_app_web/live/user_*_live.ex
# - Components: lib/my_app_web/controllers/user_session_controller.ex
# - Plugs: lib/my_app_web/user_auth.ex
# - Tests: test/my_app/accounts_test.exs, test/my_app_web/live/user_*_live_test.exs
mix ecto.gen.migration add_username_to_users
defmodule MyApp.Repo.Migrations.AddUsernameToUsers do
use Ecto.Migration
def change do
alter table(:users) do
add :username, :string, null: false
end
create unique_index(:users, [:username])
end
end
defmodule MyApp.Accounts.User do
schema "users" do
field :email, :string
field :username, :string # Add new field
field :password, :string, virtual: true, redact: true
field :hashed_password, :string, redact: true
field :confirmed_at, :utc_datetime
timestamps()
end
# Update registration_changeset to include username
def registration_changeset(user, attrs, opts \\ []) do
user
|> cast(attrs, [:email, :username, :password])
|> validate_required([:username])
|> validate_username()
|> validate_email(opts)
|> validate_password(opts)
end
defp validate_username(changeset) do
changeset
|> validate_required([:username])
|> validate_format(:username, ~r/^[a-zA-Z0-9_]+$/,
message: "only letters, numbers, and underscores"
)
|> validate_length(:username, min: 3, max: 30)
|> unsafe_validate_unique(:username, MyApp.Repo)
|> unique_constraint(:username)
end
end
# In user_registration_live.ex — update the form
def render(assigns) do
~H"""
<.simple_form for={@form} id="registration_form" phx-submit="save" phx-change="validate">
<.input field={@form[:email]} type="email" label="Email" required />
<.input field={@form[:username]} type="text" label="Username" required />
<.input field={@form[:password]} type="password" label="Password" required />
<:actions>
<.button phx-disable-with="Creating account..." class="w-full">
Create an account
</.button>
</:actions>
</.simple_form>
"""
end
# Update the save handler to pass username
def handle_event("save", %{"user" => user_params}, socket) do
case Accounts.register_user(user_params) do
{:ok, user} ->
# ... existing logic
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign_form(socket, Map.put(changeset, :action, :validate))}
end
end
This is the most commonly missed step. Every test that creates a user will break if fixtures don't include new required fields.
defmodule MyApp.AccountsFixtures do
def unique_user_email, do: "user#{System.unique_integer()}@example.com"
def unique_user_username, do: "user#{System.unique_integer([:positive])}"
def valid_user_attributes(attrs \\ %{}) do
Enum.into(attrs, %{
email: unique_user_email(),
username: unique_user_username(), # Add new required field
password: "hello world!"
})
end
def user_fixture(attrs \\ %{}) do
{:ok, user} =
attrs
|> valid_user_attributes()
|> MyApp.Accounts.register_user()
# Confirm user for password-based auth
{:ok, user} =
user
|> Ecto.Changeset.change(%{confirmed_at: DateTime.utc_now(:second)})
|> MyApp.Repo.update()
user
end
end
Without confirmed_at, the generated auth code treats the user as unconfirmed. Tests that log in users will silently fail or return unexpected redirects.
# Bad — user is unconfirmed, login tests may fail
def user_fixture(attrs \\ %{}) do
{:ok, user} =
attrs
|> valid_user_attributes()
|> MyApp.Accounts.register_user()
user # Missing confirmation!
end
# Good — user is confirmed and ready for auth tests
def user_fixture(attrs \\ %{}) do
{:ok, user} =
attrs
|> valid_user_attributes()
|> MyApp.Accounts.register_user()
{:ok, user} =
user
|> Ecto.Changeset.change(%{confirmed_at: DateTime.utc_now(:second)})
|> MyApp.Repo.update()
user
end
For non-auth fields (bio, avatar, display name), create a separate profile_changeset:
# In user.ex
def profile_changeset(user, attrs) do
user
|> cast(attrs, [:bio, :display_name, :avatar_url])
|> validate_length(:bio, max: 500)
|> validate_length(:display_name, max: 50)
end
# In accounts.ex
def update_user_profile(user, attrs) do
user
|> User.profile_changeset(attrs)
|> Repo.update()
end
describe "register_user/1" do
test "requires username" do
{:error, changeset} = Accounts.register_user(%{
email: "[email protected]",
password: "validpassword123"
})
assert "can't be blank" in errors_on(changeset).username
end
test "validates username format" do
{:error, changeset} = Accounts.register_user(%{
email: "[email protected]",
username: "has spaces",
password: "validpassword123"
})
assert "only letters, numbers, and underscores" in errors_on(changeset).username
end
test "enforces unique username" do
%{username: username} = user_fixture()
{:error, changeset} = Accounts.register_user(%{
email: "[email protected]",
username: username,
password: "validpassword123"
})
assert "has already been taken" in errors_on(changeset).username
end
end
See ecto-changeset-patterns skill for advanced changeset composition.
See phoenix-liveview-auth skill for on_mount and auth redirect patterns.
See testing-essentials skill for comprehensive testing patterns.
npx claudepluginhub j-morgan6/elixir-phoenix-guide --plugin elixir-phoenix-guideProvides Phoenix authorization patterns for LiveViews and controllers: server-side ownership checks in handle_event, policy modules, scoped queries to prevent IDOR.
Implements small Phoenix changes without planning—add validations, update routes, fix components, create migrations. For single-file edits under 50 lines.
Provides Phoenix LiveView best practices: no DB queries in mount (called twice), load data in handle_params, security scopes, scoped PubSub topics, GenServer polling, async assigns, and gotchas.