# Phoenix Auth Customization

> MANDATORY when extending phx.gen.auth with custom fields. Invoke before adding usernames, profiles, or custom registration fields. Covers migrations, schema updates, fixture updates, form changes, and confirmation patterns. Trigger words: phx.gen.auth, custom fields, registration, username, profile, auth customization.

- Skill: `igmarin/phoenix-auth-customization` (Agent Skill)
- Install (CLI): `npx skillmds@latest add igmarin/phoenix-auth-customization`
- Raw SKILL.md: https://api.skillmd.com/api/skills/igmarin/phoenix-auth-customization/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: igmarin (https://skillmd.com/u/igmarin)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/igmarin/phoenix-auth-customization

---


# Phoenix Auth Customization

Use this skill when extending `phx.gen.auth` with custom fields.


Canonical FP bar: [`docs/fcis-engineering-rules.md`](../../docs/fcis-engineering-rules.md) — **Functional Core, Imperative Shell**: pure domain modules; side effects at edges. Authorization decisions should be pure checks on scope/user data; persist only at the edge.

## RULES — Follow these with no exceptions

**1.** **Never modify generated auth migrations** — create separate migrations for custom fields
**2.** **Update `registration_changeset` to cast and validate new fields** — don't create a separate changeset
**3.** **Update test fixtures when adding required fields** — missing fixture fields cause cryptic test failures
**4.** **Confirm users in test fixtures for password-based auth** — set `confirmed_at` or tests will fail
**5.** **Update both the registration form AND the `save/2` handler** — the form must send the field
**6.** **Use `unique_constraint` + database unique index for uniqueness** — never validate in application code alone


## Running phx.gen.auth

```bash
# Generate auth with LiveView (recommended)
mix phx.gen.auth Accounts User users
```


## Adding Custom Fields

### Step 1: Create a Separate Migration

```bash
mix ecto.gen.migration add_username_to_users
```

```elixir
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
```

```bash
# Run migration and verify success before proceeding
mix ecto.migrate
```

### Step 2: Update the Schema

```elixir
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
```

### Step 3: Update Test Fixtures

```elixir
def user_fixture(attrs \\ %{}) do
  {:ok, user} =
    attrs
    |> Enum.into(%{
      email: "user#{System.unique_integer([:positive])}@example.com",
      username: "user#{System.unique_integer([:positive])}",  # Add username
      password: "hello world!",
      confirmed_at: DateTime.utc_now(:second)  # Confirm for tests
    })
    |> MyApp.Accounts.register_user()

  user
end
```

```bash
# Verify fixtures pass before updating forms
mix test
```

### Step 4: Update the Registration Form

```heex
<.simple_form for={@form} phx-change="validate" phx-submit="save">
  <.input field={@form[:username]} type="text" label="Username" />
  <.input field={@form[:email]} type="email" label="Email" />
  <.input field={@form[:password]} type="password" label="Password" />

  <:actions>
    <.button>Register</.button>
  </:actions>
</.simple_form>
```


## Additional Patterns

### Profile Fields

For optional fields like `display_name` or `avatar_url`, follow the same migration → schema → fixture sequence as above. Differences from required fields:

- Omit `null: false` in the migration column definition.
- Omit `validate_required` for that field in the changeset.

### Confirmation Patterns

`phx.gen.auth` generates a `confirmed_at` field and an email-confirmation flow automatically.

- **In tests**: set `confirmed_at: DateTime.utc_now(:second)` in fixtures (Rule 4) so tests are not blocked by the confirmation gate.
- **In production**: users must click the confirmation link before `confirmed_at` is populated. Guard LiveViews with `require_authenticated_user` and check `confirmed_at` explicitly where needed.

### Extending Other Changesets

`email_changeset` and `password_changeset` follow the same cast-then-validate pattern as `registration_changeset`. Never add a parallel changeset; extend the existing one.


## Common Pitfalls

| ❌ Don't | ✅ Do |
|----------|-------|
| Edit the generated `phx.gen.auth` migration in place | Create a separate migration (e.g. `add_username_to_users`) for custom fields |
| Create a parallel changeset for the new field | Extend `registration_changeset` with `cast/3` + `validate_*` |
| Validate uniqueness only in Elixir | Pair `unique_constraint` with a database `unique_index` |
| Forget to update `user_fixture/1` | Add the required field and `confirmed_at` to the fixture |
| Add the field to the schema but not the form | Update the form input AND the `save/2` handler |
| Leave `confirmed_at` nil in test fixtures | Set `confirmed_at: DateTime.utc_now(:second)` so password auth tests pass |

---

## Integration

| Predecessor | This Skill | Successor |
|-------------|------------|-----------|
| phoenix-liveview-auth | phoenix-auth-customization | phoenix-authorization-patterns |
| ecto-changeset-patterns | phoenix-auth-customization | testing-essentials |

**Companion skills:** `phoenix-liveview-auth`, `ecto-changeset-patterns`, `testing-essentials`

