Back to skills
SKILL.md
Phoenix Expert
ASecurityExpert 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
Works with
Security analysis
100/100npx -y skills add anubhavg-icpl/vibe --skill phoenix-expert --agent claude-codeAre you the author of Phoenix Expert?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-phoenix-expert)---
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
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.