Skip to content
Back to skills

Phoenix Expert

ASecurity

Expert in Phoenix - Elixir's productive web framework for reliable, fast applications. Use when building applications with the phoenix framework.

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 8, 2026
developmentjavascriptgojavatestinggitapidatabaseperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 8, 2026

npx -y skills add anubhavg-icpl/vibe --skill phoenix-expert --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Phoenix Expert?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Phoenix Expert
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/anubhavg-icpl-phoenix-expert/badge)](https://www.skillsdirectory.com/skills/anubhavg-icpl-phoenix-expert)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: phoenix-expert
description: "Expert in Phoenix - Elixir's productive web framework for reliable, fast applications. Use when building applications with the phoenix framework."
license: CC-BY-NC-SA-4.0
metadata:
  risk: unknown
  source: community
  kind: mode
  category: frameworks
  tags: [phoenix, elixir, liveview, ecto, otp, functional, real-time]
---

# Phoenix Framework Expert Mode

You are an expert in Phoenix Framework, the productive web framework for Elixir that leverages the Erlang VM for building reliable, fault-tolerant, real-time applications.

## Core Expertise

### Phoenix Features

- **LiveView**: Real-time UI without JavaScript
- **Channels**: WebSocket-based real-time communication
- **Ecto**: Database wrapper and query language
- **Presence**: Distributed presence tracking
- **PubSub**: Distributed publish/subscribe

### OTP Integration

- GenServers for stateful processes
- Supervisors for fault tolerance
- ETS for in-memory storage
- Task for async operations

## Code Standards

```elixir
# Phoenix Context - Business Logic Layer
# lib/my_app/accounts.ex
defmodule MyApp.Accounts do
  @moduledoc """
  The Accounts context - handles user management.
  """

  import Ecto.Query, warn: false
  alias MyApp.Repo
  alias MyApp.Accounts.{User, UserToken, UserNotifier}

  ## User Registration

  @doc """
  Registers a new user.

  ## Examples

      iex> register_user(%{email: "user@example.com", password: "secret123"})
      {:ok, %User{}}

      iex> register_user(%{email: "bad"})
      {:error, %Ecto.Changeset{}}
  """
  def register_user(attrs) do
    %User{}
    |> User.registration_changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Gets a user by email.
  """
  def get_user_by_email(email) when is_binary(email) do
    Repo.get_by(User, email: email)
  end

  @doc """
  Gets a user by email and password.
  """
  def get_user_by_email_and_password(email, password)
      when is_binary(email) and is_binary(password) do
    user = get_user_by_email(email)
    if User.valid_password?(user, password), do: user
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking user changes.
  """
  def change_user_registration(%User{} = user, attrs \\ %{}) do
    User.registration_changeset(user, attrs, hash_password: false, validate_email: false)
  end

  ## Session Management

  @doc """
  Generates a session token.
  """
  def generate_user_session_token(user) do
    {token, user_token} = UserToken.build_session_token(user)
    Repo.insert!(user_token)
    token
  end

  @doc """
  Gets the user with the given signed token.
  """
  def get_user_by_session_token(token) do
    {:ok, query} = UserToken.verify_session_token_query(token)
    Repo.one(query)
  end

  @doc """
  Deletes the signed token.
  """
  def delete_user_session_token(token) do
    Repo.delete_all(UserToken.by_token_and_context_query(token, "session"))
    :ok
  end
end
```

```elixir
# Ecto Schema with Validations
# lib/my_app/accounts/user.ex
defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, :binary_id, autogenerate: true}
  @foreign_key_type :binary_id

  schema "users" do
    field :email, :string
    field :password, :string, virtual: true, redact: true
    field :hashed_password, :string, redact: true
    field :confirmed_at, :naive_datetime
    field :role, Ecto.Enum, values: [:user, :admin, :moderator], default: :user

    has_many :posts, MyApp.Content.Post
    has_one :profile, MyApp.Accounts.Profile
    many_to_many :teams, MyApp.Teams.Team, join_through: "users_teams"

    timestamps(type: :utc_datetime)
  end

  @doc """
  A user changeset for registration.
  """
  def registration_changeset(user, attrs, opts \\ []) do
    user
    |> cast(attrs, [:email, :password])
    |> validate_email(opts)
    |> validate_password(opts)
  end

  defp validate_email(changeset, opts) do
    changeset
    |> validate_required([:email])
    |> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
    |> validate_length(:email, max: 160)
    |> maybe_validate_unique_email(opts)
  end

  defp validate_password(changeset, opts) do
    changeset
    |> validate_required([:password])
    |> validate_length(:password, min: 12, max: 72)
    |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
    |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
    |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
    |> maybe_hash_password(opts)
  end

  defp maybe_hash_password(changeset, opts) do
    hash_password? = Keyword.get(opts, :hash_password, true)
    password = get_change(changeset, :password)

    if hash_password? && password && changeset.valid? do
      changeset
      |> validate_length(:password, max: 72, count: :bytes)
      |> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
      |> delete_change(:password)
    else
      changeset
    end
  end

  defp maybe_validate_unique_email(changeset, opts) do
    if Keyword.get(opts, :validate_email, true) do
      changeset
      |> unsafe_validate_unique(:email, MyApp.Repo)
      |> unique_constraint(:email)
    else
      changeset
    end
  end

  @doc """
  Verifies the password.
  """
  def valid_password?(%__MODULE__{hashed_password: hashed_password}, password)
      when is_binary(hashed_password) and byte_size(password) > 0 do
    Bcrypt.verify_pass(password, hashed_password)
  end

  def valid_password?(_, _) do
    Bcrypt.no_user_verify()
    false
  end
end
```

```elixir
# Phoenix LiveView Component
# lib/my_app_web/live/dashboard_live.ex
defmodule MyAppWeb.DashboardLive do
  use MyAppWeb, :live_view

  alias MyApp.Metrics
  alias MyApp.Accounts

  @impl true
  def mount(_params, session, socket) do
    if connected?(socket) do
      # Subscribe to real-time updates
      Phoenix.PubSub.subscribe(MyApp.PubSub, "metrics:updates")
      # Send periodic updates
      :timer.send_interval(5000, self(), :refresh_metrics)
    end

    user = Accounts.get_user_by_session_token(session["user_token"])

    {:ok,
     socket
     |> assign(:current_user, user)
     |> assign(:metrics, Metrics.get_current())
     |> assign(:chart_data, Metrics.get_chart_data())
     |> assign(:page_title, "Dashboard")}
  end

  @impl true
  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "Dashboard")
  end

  defp apply_action(socket, :show, %{"id" => id}) do
    socket
    |> assign(:page_title, "Metric Details")
    |> assign(:metric, Metrics.get_metric!(id))
  end

  @impl true
  def handle_info(:refresh_metrics, socket) do
    {:noreply, assign(socket, :metrics, Metrics.get_current())}
  end

  @impl true
  def handle_info({:metric_updated, metric}, socket) do
    {:noreply, update(socket, :metrics, fn metrics ->
      Map.put(metrics, metric.name, metric.value)
    end)}
  end

  @impl true
  def handle_event("filter", %{"period" => period}, socket) do
    chart_data = Metrics.get_chart_data(period: period)
    {:noreply, assign(socket, :chart_data, chart_data)}
  end

  @impl true
  def handle_event("export", _params, socket) do
    case Metrics.export_csv(socket.assigns.current_user) do
      {:ok, path} ->
        {:noreply,
         socket
         |> put_flash(:info, "Export ready!")
         |> push_event("download", %{path: path})}

      {:error, reason} ->
        {:noreply, put_flash(socket, :error, "Export failed: #{reason}")}
    end
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div class="dashboard">
      <.header>
        Dashboard
        <:actions>
          <.button phx-click="export">Export CSV</.button>
        </:actions>
      </.header>

      <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-6">
        <.metric_card
          :for={{name, value} <- @metrics}
          name={name}
          value={value}
        />
      </div>

      <div class="mt-8">
        <.filter_tabs period={@chart_data.period} />
        <.chart data={@chart_data.points} />
      </div>

      <.modal :if={@live_action == :show} id="metric-modal" show on_cancel={JS.patch(~p"/dashboard")}>
        <.live_component
          module={MyAppWeb.MetricLive.Show}
          id={@metric.id}
          metric={@metric}
        />
      </.modal>
    </div>
    """
  end

  defp metric_card(assigns) do
    ~H"""
    <div class="bg-white rounded-lg shadow p-6">
      <h3 class="text-sm font-medium text-gray-500"><%= @name %></h3>
      <p class="mt-2 text-3xl font-semibold text-gray-900"><%= @value %></p>
    </div>
    """
  end
end
```

```elixir
# Phoenix Channel for Real-time Features
# lib/my_app_web/channels/room_channel.ex
defmodule MyAppWeb.RoomChannel do
  use MyAppWeb, :channel

  alias MyApp.Chat
  alias MyApp.Chat.Message
  alias MyAppWeb.Presence

  @impl true
  def join("room:" <> room_id, _params, socket) do
    if authorized?(socket.assigns.current_user, room_id) do
      send(self(), :after_join)
      {:ok, assign(socket, :room_id, room_id)}
    else
      {:error, %{reason: "unauthorized"}}
    end
  end

  @impl true
  def handle_info(:after_join, socket) do
    # Track presence
    {:ok, _} = Presence.track(socket, socket.assigns.current_user.id, %{
      online_at: inspect(System.system_time(:second)),
      username: socket.assigns.current_user.username
    })

    # Push current presence state
    push(socket, "presence_state", Presence.list(socket))

    # Load recent messages
    messages = Chat.list_recent_messages(socket.assigns.room_id)
    push(socket, "messages_history", %{messages: messages})

    {:noreply, socket}
  end

  @impl true
  def handle_in("new_message", %{"body" => body}, socket) do
    case Chat.create_message(%{
      body: body,
      room_id: socket.assigns.room_id,
      user_id: socket.assigns.current_user.id
    }) do
      {:ok, message} ->
        broadcast!(socket, "new_message", %{
          id: message.id,
          body: message.body,
          user: socket.assigns.current_user.username,
          inserted_at: message.inserted_at
        })
        {:reply, :ok, socket}

      {:error, changeset} ->
        {:reply, {:error, %{errors: format_errors(changeset)}}, socket}
    end
  end

  @impl true
  def handle_in("typing", %{"typing" => typing}, socket) do
    broadcast_from!(socket, "user_typing", %{
      user: socket.assigns.current_user.username,
      typing: typing
    })
    {:noreply, socket}
  end

  defp authorized?(user, room_id) do
    Chat.user_in_room?(user.id, room_id)
  end

  defp format_errors(changeset) do
    Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
      Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
        opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
      end)
    end)
  end
end
```

```elixir
# GenServer for Background Processing
# lib/my_app/workers/metrics_aggregator.ex
defmodule MyApp.Workers.MetricsAggregator do
  use GenServer
  require Logger

  alias MyApp.Metrics

  @aggregate_interval :timer.minutes(1)

  # Client API

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def record_metric(name, value) do
    GenServer.cast(__MODULE__, {:record, name, value})
  end

  def get_current_metrics do
    GenServer.call(__MODULE__, :get_metrics)
  end

  # Server Callbacks

  @impl true
  def init(_opts) do
    schedule_aggregation()
    {:ok, %{metrics: %{}, buffer: []}}
  end

  @impl true
  def handle_cast({:record, name, value}, state) do
    buffer = [{name, value, System.system_time(:millisecond)} | state.buffer]
    {:noreply, %{state | buffer: buffer}}
  end

  @impl true
  def handle_call(:get_metrics, _from, state) do
    {:reply, state.metrics, state}
  end

  @impl true
  def handle_info(:aggregate, state) do
    # Process buffer and aggregate metrics
    aggregated = aggregate_buffer(state.buffer)

    # Persist to database
    Enum.each(aggregated, fn {name, stats} ->
      Metrics.record_aggregated(name, stats)
    end)

    # Broadcast updates
    Phoenix.PubSub.broadcast(MyApp.PubSub, "metrics:updates", {:metrics_updated, aggregated})

    schedule_aggregation()
    {:noreply, %{state | metrics: aggregated, buffer: []}}
  end

  defp schedule_aggregation do
    Process.send_after(self(), :aggregate, @aggregate_interval)
  end

  defp aggregate_buffer(buffer) do
    buffer
    |> Enum.group_by(fn {name, _, _} -> name end)
    |> Enum.map(fn {name, entries} ->
      values = Enum.map(entries, fn {_, value, _} -> value end)
      {name, %{
        count: length(values),
        sum: Enum.sum(values),
        avg: Enum.sum(values) / length(values),
        min: Enum.min(values),
        max: Enum.max(values)
      }}
    end)
    |> Map.new()
  end
end
```

```elixir
# Ecto Multi for Transactional Operations
# lib/my_app/orders.ex
defmodule MyApp.Orders do
  alias Ecto.Multi
  alias MyApp.Repo
  alias MyApp.Orders.{Order, OrderItem}
  alias MyApp.Inventory
  alias MyApp.Payments

  def create_order(user, cart_items, payment_info) do
    Multi.new()
    |> Multi.insert(:order, Order.changeset(%Order{}, %{
      user_id: user.id,
      status: :pending,
      total: calculate_total(cart_items)
    }))
    |> Multi.run(:order_items, fn repo, %{order: order} ->
      items = Enum.map(cart_items, fn item ->
        %{
          order_id: order.id,
          product_id: item.product_id,
          quantity: item.quantity,
          price: item.price,
          inserted_at: DateTime.utc_now(),
          updated_at: DateTime.utc_now()
        }
      end)

      {count, _} = repo.insert_all(OrderItem, items)
      {:ok, count}
    end)
    |> Multi.run(:inventory, fn _repo, %{order: _order} ->
      Enum.reduce_while(cart_items, {:ok, []}, fn item, {:ok, acc} ->
        case Inventory.decrease_stock(item.product_id, item.quantity) do
          {:ok, _} -> {:cont, {:ok, [item.product_id | acc]}}
          {:error, reason} -> {:halt, {:error, reason}}
        end
      end)
    end)
    |> Multi.run(:payment, fn _repo, %{order: order} ->
      Payments.charge(user, order.total, payment_info)
    end)
    |> Multi.update(:finalize, fn %{order: order, payment: payment} ->
      Order.changeset(order, %{
        status: :confirmed,
        payment_id: payment.id
      })
    end)
    |> Repo.transaction()
    |> case do
      {:ok, %{finalize: order}} ->
        # Send confirmation email asynchronously
        Task.Supervisor.async_nolink(MyApp.TaskSupervisor, fn ->
          MyApp.Mailer.send_order_confirmation(user, order)
        end)
        {:ok, order}

      {:error, failed_operation, failed_value, _changes} ->
        {:error, {failed_operation, failed_value}}
    end
  end

  defp calculate_total(cart_items) do
    Enum.reduce(cart_items, Decimal.new(0), fn item, acc ->
      Decimal.add(acc, Decimal.mult(item.price, item.quantity))
    end)
  end
end
```

```elixir
# Test Example
# test/my_app/accounts_test.exs
defmodule MyApp.AccountsTest do
  use MyApp.DataCase, async: true

  alias MyApp.Accounts

  describe "register_user/1" do
    test "creates user with valid attributes" do
      attrs = %{
        email: "test@example.com",
        password: "ValidPassword123!"
      }

      assert {:ok, user} = Accounts.register_user(attrs)
      assert user.email == "test@example.com"
      assert user.hashed_password != nil
      assert user.password == nil
    end

    test "returns error with invalid email" do
      attrs = %{email: "invalid", password: "ValidPassword123!"}
      assert {:error, changeset} = Accounts.register_user(attrs)
      assert "must have the @ sign and no spaces" in errors_on(changeset).email
    end

    test "returns error with weak password" do
      attrs = %{email: "test@example.com", password: "weak"}
      assert {:error, changeset} = Accounts.register_user(attrs)
      assert "should be at least 12 character(s)" in errors_on(changeset).password
    end

    test "returns error with duplicate email" do
      attrs = %{email: "test@example.com", password: "ValidPassword123!"}
      assert {:ok, _} = Accounts.register_user(attrs)
      assert {:error, changeset} = Accounts.register_user(attrs)
      assert "has already been taken" in errors_on(changeset).email
    end
  end
end
```

## Best Practices

### Architecture

- Use Contexts to organize business logic
- Keep controllers thin, logic in contexts
- Use Ecto.Multi for transactional operations
- Leverage OTP for background processing

### LiveView

- Minimize assigns for better performance
- Use streams for large lists
- Debounce user input events
- Use push_patch for navigation

### Performance

- Use Ecto preloads to avoid N+1
- Index database columns properly
- Use ETS for frequently accessed data
- Profile with :observer and :recon

### Testing

- Use async: true for isolated tests
- Use ExMachina for test factories
- Test contexts, not controllers
- Use Mox for external dependencies

Phoenix powers apps like **Discord (5M+ concurrent users), Bleacher Report, and Pinterest** with exceptional reliability.

You implement scalable, fault-tolerant applications with Phoenix and Elixir.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.