Phoenix and LiveView skill for Codex

Contexts, streams and PubSub without data leaks. Codex keeps Phoenix web code behind context functions that take the current scope, so every query is filtered by owner. LiveViews hold collections in streams, load slow data with assign_async, subscribe only once connected, and re-check ownership on every event. Broadcasts happen after commit, channel joins are authorized, and tests cover both the context and the LiveView.

  • Backend & APIs
  • $phoenix-elixir
  • 191 lines
  • 2 files
  • ~1,850 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- phoenix-elixir

Installs to ~/.agents/skills/phoenix-elixir/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $phoenix-elixir, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds Phoenix 1.7 and 1.8 applications in Elixir - context modules as the boundary around Ecto schemas, changesets and Ecto.Multi, LiveView 1.x with streams, assign_async and handle_params, PubSub broadcasts, channels with join authorization and Presence, scopes from phx.gen.auth, and ExUnit tests with ConnCase, DataCase and Phoenix.LiveViewTest. Use when adding a feature to a Phoenix app, writing a LiveView, wiring real-time updates, fixing N+1 preloads or double queries on mount, or testing LiveViews and channels.

Use it when

  • Adding a feature, context or LiveView to a Phoenix 1.7 or 1.8 application
  • Wiring real-time updates with PubSub, channels or Presence
  • LiveView memory, double mount queries or N+1 preloads hurt performance
  • Testing contexts, controllers, LiveViews or channels with ExUnit

Use something else when

  • The question is GenServer, supervision tree or OTP design: use elixir-otp
  • You need a plain WebSocket design guide independent of Phoenix: use websockets-realtime

How to check the result

mix compile with warnings as errors and mix test pass, including a LiveView test that drives the UI event and a test proving another user's records are not reachable.

The mistake it prevents

Running expensive queries or PubSub subscriptions directly in mount, which executes twice per page load and subscribes on the static render.

~/.agents/skills/phoenix-elixir/SKILL.md

--- name: phoenix-elixir description: Builds Phoenix 1.7 and 1.8 applications in Elixir - context modules as the boundary around Ecto schemas, changesets and Ecto.Multi, LiveView 1.x with… license: MIT ---

Done well means: web modules call context functions and never Repo directly, every write goes through a changeset, LiveViews keep large collections in streams and do their expensive work only once, real-time updates go through PubSub with authorization on subscribe, and tests cover the context and the LiveView or controller separately.

Before changing anything

  1. Read AGENTS.md (Phoenix 1.8 generates one with project rules) and mix.exs for the Phoenix, LiveView and Ecto versions.
  2. Look at lib/<app>/ (contexts) and lib/<app>_web/ (router, live, controllers, components). Note whether phx.gen.auth scopes are in use (current_scope assigns).
  3. Baseline: mix deps.get (network), mix compile --warnings-as-errors, mix test. In 1.8 projects prefer the generated mix precommit alias if it exists.

Contexts

A context is the public API for one area of the domain. Generators give a correct start:

mix phx.gen.context Catalog Product products name:string price:decimal
mix phx.gen.live Catalog Product products name:string price:decimal   # context + LiveViews
mix phx.gen.json Catalog Product products name:string --no-context    # reuse existing context
defmodule Shop.Orders do
  import Ecto.Query
  alias Shop.Repo
  alias Shop.Orders.{Order, LineItem}

  def list_orders(%Scope{user: user}) do
    Order
    |> where(user_id: ^user.id)
    |> order_by(desc: :inserted_at)
    |> preload(:line_items)
    |> Repo.all()
  end

  def place_order(%Scope{} = scope, attrs) do
    Ecto.Multi.new()
    |> Ecto.Multi.insert(:order, Order.changeset(%Order{user_id: scope.user.id}, attrs))
    |> Ecto.Multi.run(:stock, fn repo, %{order: order} -> reserve_stock(repo, order) end)
    |> Repo.transact()
    |> case do
      {:ok, %{order: order}} ->
        broadcast(scope, {:order_placed, order})
        {:ok, order}
      {:error, :order, changeset, _} -> {:error, changeset}
      {:error, :stock, reason, _} -> {:error, reason}
    end
  end
end

Repo.transact/2 (Ecto 3.13+) takes a Multi or a function returning {:ok, _} / {:error, _}; on older Ecto use Repo.transaction/1. For a fixed sequence of steps a plain function inside Repo.transact reads better than a Multi.

  • Pass the scope (or current user) as the first argument so every query is filtered by ownership. A query without a user or tenant filter in a multi-tenant app is a data leak.
  • Changesets: cast only the fields the user may set, validate_*, and the matching *_constraint (unique_constraint(:email)) so database errors become changeset errors.
  • Preload in the context (preload: [:line_items]), never lazily in templates; Ecto raises Ecto.Association.NotLoaded errors instead of silently querying, which is good.
  • Broadcast after commit, never inside the transaction.

LiveView

Lifecycle: mount/3 runs twice (once for the static HTTP render, once when the socket connects), then handle_params/3, then events.

defmodule ShopWeb.OrderLive.Index do
  use ShopWeb, :live_view
  alias Shop.Orders

  @impl true
  def mount(_params, _session, socket) do
    scope = socket.assigns.current_scope
    if connected?(socket), do: Orders.subscribe(scope)

    {:ok,
     socket
     |> assign(:page_title, "Orders")
     |> stream(:orders, Orders.list_orders(scope))
     |> assign_async(:stats, fn -> {:ok, %{stats: Orders.stats(scope)}} end)}
  end

  @impl true
  def handle_info({:order_placed, order}, socket) do
    {:noreply, stream_insert(socket, :orders, order, at: 0)}
  end

  @impl true
  def handle_event("cancel", %{"id" => id}, socket) do
    scope = socket.assigns.current_scope
    case Orders.cancel_order(scope, id) do
      {:ok, order} -> {:noreply, stream_insert(socket, :orders, order)}
      {:error, reason} -> {:noreply, put_flash(socket, :error, "Could not cancel: #{inspect(reason)}")}
    end
  end
end
<div id="orders" phx-update="stream">
  <div :for={{dom_id, order} <- @streams.orders} id={dom_id}>
    {order.reference} <button phx-click="cancel" phx-value-id={order.id}>Cancel</button>
  </div>
</div>
<.async_result :let={stats} assign={@stats}>
  <:loading>Loading stats...</:loading>
  {stats.total} orders
</.async_result>
SituationUse
List that grows or updates in placestream/3 + stream_insert / stream_delete (not a list assign)
Slow data that should not block first paintassign_async/3 or start_async/3
State that belongs in the URL (filters, page, tab)push_patch + handle_params/3
Formto_form(changeset), phx-change="validate" with action: :validate, phx-submit
Subscriptionif connected?(socket); never subscribe on the static render
  • Treat every handle_event payload as untrusted: re-check ownership through the scope. A hidden phx-value-id can be edited in the browser.
  • Do not store large structs in assigns; each assign lives in the LiveView process memory for the whole session.
  • HEEx in 1.7.14+/LiveView 1.0 accepts {expr} interpolation in bodies and attributes; older templates use <%= %>. Match the project.

Channels and PubSub

def subscribe(%Scope{user: user}), do: Phoenix.PubSub.subscribe(Shop.PubSub, "orders:user:#{user.id}")
defp broadcast(%Scope{user: user}, msg), do: Phoenix.PubSub.broadcast(Shop.PubSub, "orders:user:#{user.id}", msg)

For non-LiveView clients (mobile, other SPAs), channels:

def join("room:" <> room_id, _params, socket) do
  if Chat.member?(socket.assigns.user_id, room_id),
    do: {:ok, assign(socket, :room_id, room_id)},
    else: {:error, %{reason: "unauthorized"}}
end

Authenticate in UserSocket.connect/3 (verify a Phoenix.Token from params), authorize in every join/3. Use Phoenix.Presence for who-is-online instead of hand-rolled ETS.

Testing

test "cancelling an order updates the row", %{conn: conn, scope: scope} do
  order = order_fixture(scope)
  {:ok, view, _html} = live(conn, ~p"/orders")
  assert view |> element("#orders-#{order.id} button", "Cancel") |> render_click() =~ "cancelled"
end
  • Context tests in DataCase (sandboxed, async: true where possible); web tests in ConnCase.
  • Test that a second user cannot read or mutate the first user’s records through the context and through the LiveView event.
  • Async assigns: render_async(view) before asserting on their content.

Checklist

  • Web layer calls contexts only; contexts take scope or user first
  • Changesets cast an allow-list and declare constraints
  • Collections in streams; subscriptions only when connected
  • Broadcast after commit; channel joins authorized
  • mix format --check-formatted, mix compile --warnings-as-errors, mix test pass

Gotchas

  • Expensive queries in mount/3 run twice per page load; move them behind connected?/1, assign_async, or handle_params as appropriate.
  • Repo.preload inside Enum.map is N+1; preload the list once.
  • Runtime config belongs in config/runtime.exs; values in config/prod.exs are baked in at compile time and ignore environment variables set at boot.
  • Running mix ecto.reset or migrations against anything but a local database drops or alters real data; confirm with the user first.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Phoenix and LiveView"
  short_description: "Contexts, streams and PubSub without data leaks"
  default_prompt: "$phoenix-elixir Add an orders page to our Phoenix app with live updates when a new order arrives"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem