SuperAPI AI Directory

phoenix-auth-customization

Coding & development

View repository

Updated 10-16 06:17

About this skill

Phoenix Auth Customization

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

Canonical FP bar: 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

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

Adding Custom Fields

Step 1: Create a Separate 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
      add :username, :string, null: false
    end

    create unique_index(:users, [:username])
  end
end
# Run migration and verify success before proceeding
mix ecto.migrate

Step 2: Update the Schema

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

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
# Verify fixtures pass before updating forms
mix test

Step 4: Update the Registration Form

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