Claude
Skills
Sign in
Back

phoenix-framework

Included with Lifetime
$97 forever

Guide for Phoenix web applications. Use when building Phoenix apps, implementing LiveView, designing contexts, setting up channels, or integrating Tidewave MCP dev tools.

AI Agents

What this skill does


# Phoenix Framework Development

This skill activates when working with Phoenix web applications, including setup, development, LiveView, contexts, controllers, and channels.

**Current versions**: Phoenix 1.8.x (current: 1.8.5), Phoenix LiveView 1.1.x (current: 1.1.27). Requires Elixir 1.14+, Erlang/OTP 25+.

## When to Use This Skill

Activate this skill when:
- Creating or modifying Phoenix applications
- Implementing LiveView components or pages
- Working with Phoenix contexts and business logic
- Building real-time features with channels or LiveView
- Configuring Phoenix routers, plugs, or endpoints
- Troubleshooting Phoenix-specific issues

## Phoenix Project Structure

Follow Phoenix conventions:

```
lib/
  my_app/           # Business logic and contexts
    accounts/       # Domain contexts
    repo.ex
  my_app_web/       # Web interface
    controllers/
    live/           # LiveView modules
    components/     # Function components
    router.ex
    endpoint.ex
```

## Runtime configuration

`config/runtime.exs` runs at every boot (dev, test, prod). Three settings have caused production outages when configured incorrectly:

### Phoenix Endpoint `:ip` bind config

The Endpoint's `:ip` bind tuple MUST be set at the TOP LEVEL of `runtime.exs`, env-driven, with a default of `{0, 0, 0, 0}` (all IPv4 interfaces). Do NOT gate it inside `if port = System.get_env("PORT") do ... end`, and do NOT override it later in a prod-only block that hardcodes `{0,0,0,0,0,0,0,0}` (IPv6 wildcard).

CORRECT:

```elixir
# config/runtime.exs (top level, unconditional)
bind_address =
  case System.get_env("BIND_ADDRESS", "0.0.0.0") do
    "0.0.0.0" -> {0, 0, 0, 0}
    "::" -> {0, 0, 0, 0, 0, 0, 0, 0}
    other -> other |> String.to_charlist() |> :inet.parse_address() |> elem(1)
  end

config :<app>, <App>Web.Endpoint, http: [ip: bind_address, port: ...]
```

WRONG (gates on PORT):

```elixir
if port = System.get_env("PORT") do
  config :<app>, <App>Web.Endpoint, http: [ip: {0, 0, 0, 0}, port: ...]
end
# Without PORT, Phoenix falls back to its default 127.0.0.1 — unreachable.
```

WRONG (later prod block clobbers env-driven setting):

```elixir
# Top-level env-driven config OK ...
if config_env() == :prod do
  config :<app>, <App>Web.Endpoint, http: [ip: {0, 0, 0, 0, 0, 0, 0, 0}, ...]
end
# IPv6 wildcard binds *:port IPv6 only — IPv4-only overlay networks (Tailscale on macOS) cannot reach it.
```

Default to IPv4 because most overlay networks route IPv4 first on macOS. Add IPv6 only when the deployment target explicitly requires it.

### PHX_HOST matches the public DNS name

Set `PHX_HOST` to the externally-resolvable hostname the load balancer presents to the browser. LiveView's websocket upgrade matches Origin against `PHX_HOST`; a mismatch causes the LiveView socket to fail with `403` and the page reverts to a dead static render.

### Dev-server restart after `lib/**/*.ex` changes

Phoenix live reload covers `.heex` / `.html.eex` templates and `assets/` reliably. It does NOT reliably pick up changes to:

- `lib/**/*.ex` — compiled Elixir modules
- `mix.exs` or any dependency change
- `config/*.exs`, especially `runtime.exs`
- NIFs, native deps, BEAM-level plugins
- DB migrations that change already-loaded schema

After any of those changes, restart the dev server (kill + relaunch the `mise run dev` session or equivalent). `/api/info`-style endpoints report the `git_sha` at server-start time, NOT the currently-compiled-in-memory code, and cannot distinguish stale-dev from fresh-dev. Restart is the only reliable signal.

## Context-Driven Design

Organize business logic into contexts (bounded domains):

### Creating Contexts

Generate contexts with related schemas:
```bash
mix phx.gen.context Accounts User users email:string name:string
```

Structure contexts to encapsulate business logic:

```elixir
defmodule MyApp.Accounts do
  @moduledoc """
  The Accounts context - manages user accounts and authentication.
  """

  alias MyApp.Repo
  alias MyApp.Accounts.User

  def list_users do
    Repo.all(User)
  end

  def get_user!(id), do: Repo.get!(User, id)

  def create_user(attrs \\ %{}) do
    %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
  end

  def update_user(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> Repo.update()
  end
end
```

### Context Best Practices

- Keep contexts focused on a single domain
- Avoid cross-context dependencies when possible
- Use public API functions, not direct Repo access in web layer
- Name contexts after business domains, not technical layers

## LiveView Development

LiveView enables rich, real-time experiences without writing JavaScript.

### LiveView Lifecycle

Understand the mount → handle_event → render cycle:

```elixir
defmodule MyAppWeb.UserLive.Index do
  use MyAppWeb, :live_view

  alias MyApp.Accounts

  @impl true
  def mount(_params, _session, socket) do
    # Runs on initial page load and live connection
    {:ok, assign(socket, :users, list_users())}
  end

  @impl true
  def handle_params(params, _url, socket) do
    # Runs after mount and on live patch
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    user = Accounts.get_user!(id)
    {:ok, _} = Accounts.delete_user(user)

    {:noreply, assign(socket, :users, list_users())}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <.table rows={@users} id="users">
        <:col :let={user} label="Name"><%= user.name %></:col>
        <:col :let={user} label="Email"><%= user.email %></:col>
        <:action :let={user}>
          <.button phx-click="delete" phx-value-id={user.id}>Delete</.button>
        </:action>
      </.table>
    </div>
    """
  end

  defp list_users do
    Accounts.list_users()
  end
end
```

### LiveView Best Practices

- Use `mount/3` for initial data loading
- Handle route changes in `handle_params/3`
- Keep renders fast - compute in event handlers, not render
- Use `assign_new/3` for expensive computations
- Prefer LiveView over JavaScript for interactive UIs
- Use `phx-debounce` and `phx-throttle` for frequent events

### Function Components

Create reusable components:

```elixir
defmodule MyAppWeb.Components.UserCard do
  use Phoenix.Component

  attr :user, :map, required: true
  attr :class, :string, default: ""

  def user_card(assigns) do
    ~H"""
    <div class={"card " <> @class}>
      <h3><%= @user.name %></h3>
      <p><%= @user.email %></p>
    </div>
    """
  end
end
```

Use with `<.user_card user={@current_user} />` in templates.

### Form Handling

Use changesets for validation:

```elixir
@impl true
def mount(_params, _session, socket) do
  changeset = Accounts.change_user(%User{})
  {:ok, assign(socket, form: to_form(changeset))}
end

@impl true
def handle_event("validate", %{"user" => user_params}, socket) do
  changeset =
    %User{}
    |> Accounts.change_user(user_params)
    |> Map.put(:action, :validate)

  {:noreply, assign(socket, form: to_form(changeset))}
end

@impl true
def handle_event("save", %{"user" => user_params}, socket) do
  case Accounts.create_user(user_params) do
    {:ok, user} ->
      {:noreply,
       socket
       |> put_flash(:info, "User created successfully")
       |> push_navigate(to: ~p"/users/#{user}")}

    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, assign(socket, form: to_form(changeset))}
  end
end

def render(assigns) do
  ~H"""
  <.form for={@form} phx-change="validate" phx-submit="save">
    <.input field={@form[:name]} label="Name" />
    <.input field={@form[:email]} label="Email" type="email" />
    <.button>Save</.button>
  </.form>
  """
end
```

## Routing

### Route Organization

Structure routes logically:

```elixir
defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, htm

Related in AI Agents