AgentStack
SKILL verified MIT Self-run

Phoenix

skill-vinnie357-claude-skills-phoenix · by vinnie357

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

No reviews yet
0 installs
3 views
0.0% view→install

Install

$ agentstack add skill-vinnie357-claude-skills-phoenix

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README — it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-vinnie357-claude-skills-phoenix)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
2d ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming — see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps — measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Phoenix? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

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:

# 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 :, Web.Endpoint, http: [ip: bind_address, port: ...]

WRONG (gates on PORT):

if port = System.get_env("PORT") do
  config :, 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):

# Top-level env-driven config OK ...
if config_env() == :prod do
  config :, 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:

mix phx.gen.context Accounts User users email:string name:string

Structure contexts to encapsulate business logic:

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:

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"""
    
      
        
        
        
          Delete
        
      
    
    """
  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:

defmodule MyAppWeb.Components.UserCard do
  use Phoenix.Component

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

  def user_card(assigns) do
    ~H"""
     @class}>
      
      
    
    """
  end
end

Use with `` in templates.

Form Handling

Use changesets for validation:

@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"""
  
    
    
    Save
  
  """
end

Routing

Route organization (pipelines, scopes) and LiveView routes with live actions: see references/routing-and-channels.md.

Channels and PubSub

Custom Channels for real-time protocols and Phoenix.PubSub for cross-process broadcasts: see references/routing-and-channels.md.

Testing Phoenix Applications

Controller Tests

defmodule MyAppWeb.UserControllerTest do
  use MyAppWeb.ConnCase, async: true

  test "GET /users", %{conn: conn} do
    conn = get(conn, ~p"/users")
    assert html_response(conn, 200) =~ "Listing Users"
  end
end

LiveView Tests

defmodule MyAppWeb.UserLiveTest do
  use MyAppWeb.ConnCase

  import Phoenix.LiveViewTest

  test "displays users", %{conn: conn} do
    user = insert(:user)

    {:ok, view, html} = live(conn, ~p"/users")

    assert html =~ user.name
    assert has_element?(view, "#user-#{user.id}")
  end

  test "creates user", %{conn: conn} do
    {:ok, view, _html} = live(conn, ~p"/users/new")

    assert view
           |> form("#user-form", user: %{name: "Alice", email: "alice@example.com"})
           |> render_submit()

    assert_patch(view, ~p"/users")
  end
end

Channel Tests

defmodule MyAppWeb.RoomChannelTest do
  use MyAppWeb.ChannelCase

  test "broadcasts are pushed to the client", %{socket: socket} do
    {:ok, _, socket} = subscribe_and_join(socket, "room:lobby", %{})

    broadcast_from!(socket, "new_msg", %{body: "test"})
    assert_broadcast "new_msg", %{body: "test"}
  end
end

Common Patterns

Preloading associations, pagination, and handling LiveView file uploads: see references/common-patterns.md.

Performance Optimization

Database Query Optimization

  • Use preload/2 to avoid N+1 queries
  • Add database indexes for frequently queried fields
  • Use select/3 to load only needed fields
  • Consider using Repo.stream/2 for large datasets

LiveView Performance

  • Move expensive computations to handle_event or background jobs
  • Use assign_new/3 for computed values
  • Implement handle_continue/2 for async operations after mount
  • Use temporary assigns for large lists: assign(socket, :items, temporary: true)

Caching

Use Cachex or ETS for caching:

def get_user!(id) do
  Cachex.fetch(:users, id, fn ->
    {:commit, Repo.get!(User, id)}
  end)
end

Security Best Practices

  • Always validate and sanitize user input through changesets
  • Use CSRF protection (enabled by default)
  • Implement rate limiting for APIs
  • Use put_secure_browser_headers plug
  • Validate file uploads (type, size, content)
  • Use prepared statements (Ecto does this automatically)
  • Implement proper authentication and authorization

OpenAPI Contract-First

Define the OpenAPI spec for any HTTP API surface BEFORE implementing the controller. Clients (and Tier 2 test authors) generate from the spec; the implementation makes the spec true.

In Elixir, use open_api_spex for spec definitions:

defmodule Web.UserSpec do
  alias OpenApiSpex.{Schema, Operation}

  @user_schema %Schema{
    type: :object,
    required: [:id, :email],
    properties: %{
      id: %Schema{type: :integer},
      email: %Schema{type: :string, format: :email}
    }
  }
end

Wire the spec to the controller via @spec macros and serve it at /api/openapi. Tests assert response shapes against the spec rather than against hand-rolled JSON expectations; client SDKs regenerate from the spec on every spec change.

Tidewave MCP Dev Tools

Tidewave connects AI coding assistants to running Phoenix applications via MCP, exposing runtime introspection tools (Ecto schemas, code execution, docs, logs, SQL queries).

Add to mix.exs: {:tidewave, "~> 0.5", only: :dev}

Add to endpoint.ex before code_reloading?: plug Tidewave (guarded by if Mix.env() == :dev)

Connect Claude Code: claude mcp add --transport http tidewave http://localhost:4000/tidewave/mcp

Tidewave is dev-only — never deploy to production. It only accepts localhost requests by default.

For full setup, MCP tools reference, CLI app, editor configs, LiveView annotations, and troubleshooting, see /elixir:tidewave or references/tidewave.md.

Key Principles

  • Context boundaries: Keep business logic in contexts, not controllers/LiveViews
  • LiveView first: Prefer LiveView over JavaScript for interactive features
  • Changesets for validation: Always validate through Ecto changesets
  • Pub/Sub for communication: Use Phoenix.PubSub for cross-process updates
  • Test at boundaries: Test contexts, controllers, and LiveViews separately
  • Follow conventions: Use Phoenix generators and follow established patterns

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.