git clone https://github.com/MacPhobos/research-mind
T=$(mktemp -d) && git clone --depth=1 https://github.com/MacPhobos/research-mind "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/toolchains-elixir-frameworks-phoenix-liveview" ~/.claude/skills/macphobos-research-mind-phoenix-liveview && rm -rf "$T"
.claude/skills/toolchains-elixir-frameworks-phoenix-liveview/SKILL.mdPhoenix + LiveView (Elixir/BEAM)
Phoenix builds on Elixir and the BEAM VM to deliver fault-tolerant, real-time web applications with minimal JavaScript. LiveView keeps UI state on the server while streaming HTML diffs over WebSockets. The BEAM provides lightweight processes, supervision trees, hot code upgrades, and soft-realtime scheduling.
Key ideas
- OTP supervision keeps web, data, and background processes isolated and restartable.
- Contexts encode domain boundaries (e.g., Accounts, Billing) around Ecto schemas and queries.
- LiveView renders HTML on the server, syncing UI state over WebSockets with minimal client code.
- PubSub + Presence enable fan-out updates, tracking, and collaboration features.
Environment and Project Setup
# Erlang + Elixir via asdf (recommended) asdf install erlang 27.0 asdf install elixir 1.17.3 asdf global erlang 27.0 elixir 1.17.3 # Install Phoenix generator mix archive.install hex phx_new # Create project with LiveView + Ecto + esbuild mix phx.new my_app --live cd my_app mix deps.get mix ecto.create mix phx.server
Project layout (key pieces):
— OTP supervision tree (Repo, Endpoint, Telemetry, PubSub, Oban, etc.)lib/my_app/application.ex
— Endpoint, plugs, sockets, LiveView configlib/my_app_web/endpoint.ex
— Pipelines, scopes, routes, LiveSessionslib/my_app_web/router.ex
— Contexts (domain modules) and Ecto schemaslib/my_app/
— Testing helpers for Ecto + Phoenixtest/support/{conn_case,data_case}.ex
BEAM + OTP Essentials
Supervision tree (application.ex): keep short, isolated children.
def start(_type, _args) do children = [ MyApp.Repo, {Phoenix.PubSub, name: MyApp.PubSub}, MyAppWeb.Endpoint, {Oban, Application.fetch_env!(:my_app, Oban)}, MyApp.Metrics ] Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor) end
GenServer pattern: wrap stateful services.
defmodule MyApp.Counter do use GenServer def start_link(initial \\ 0), do: GenServer.start_link(__MODULE__, initial, name: __MODULE__) def increment(), do: GenServer.call(__MODULE__, :inc) @impl true def handle_call(:inc, _from, state) do new_state = state + 1 {:reply, new_state, new_state} end end
BEAM principles
- Prefer many small processes; processes are cheap and isolated.
- Supervise everything with clear restart strategies.
- Use message passing (
/GenServer.cast
) to avoid shared state.send - Use ETS/Cachex for in-memory caches; keep them supervised.
Phoenix Anatomy and Routing
Pipelines and scopes (router.ex): keep browser/api concerns separated.
defmodule MyAppWeb.Router do use MyAppWeb, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_live_flash plug :protect_from_forgery plug :put_secure_browser_headers plug :fetch_current_user end pipeline :api do plug :accepts, ["json"] end scope "/", MyAppWeb do pipe_through :browser live "/", HomeLive resources "/users", UserController end scope "/api", MyAppWeb do pipe_through :api resources "/users", Api.UserController, except: [:new, :edit] end end
Plugs: composable request middleware. Keep plugs pure and short; prefer pipeline plugs over controller plugs when cross-cutting.
Contexts and Ecto
Schema + changeset
defmodule MyApp.Accounts.User do use Ecto.Schema import Ecto.Changeset schema "users" do field :email, :string field :hashed_password, :string field :confirmed_at, :naive_datetime timestamps() end def registration_changeset(user, attrs) do user |> cast(attrs, [:email, :password]) |> validate_required([:email, :password]) |> validate_format(:email, ~r/@/) |> validate_length(:password, min: 12) |> unique_constraint(:email) |> put_password_hash() end defp put_password_hash(%{valid?: true} = changeset), do: put_change(changeset, :hashed_password, Argon2.hash_pwd_salt(get_change(changeset, :password))) defp put_password_hash(changeset), do: changeset end
Context API
defmodule MyApp.Accounts do import Ecto.Query, warn: false alias MyApp.{Repo, Accounts.User} def list_users, do: Repo.all(User) def get_user!(id), do: Repo.get!(User, id) def register_user(attrs) do %User{} |> User.registration_changeset(attrs) |> Repo.insert() end end
Transactions with Ecto.Multi
alias Ecto.Multi def register_and_welcome(attrs) do Multi.new() |> Multi.insert(:user, User.registration_changeset(%User{}, attrs)) |> Multi.run(:welcome_email, fn _repo, %{user: user} -> MyApp.Mailer.deliver_welcome(user) {:ok, :sent} end) |> Repo.transaction() end
LiveView Patterns
LiveView module (stateful UI on server)
defmodule MyAppWeb.CounterLive do use MyAppWeb, :live_view def mount(_params, _session, socket) do {:ok, assign(socket, count: 0)} end def handle_event("inc", _params, socket) do {:noreply, update(socket, :count, &(&1 + 1))} end def render(assigns) do ~H""" <div class="space-y-4"> <p class="text-lg">Count: <%= @count %></p> <button phx-click="inc" class="btn">Increment</button> </div> """ end end
HEEx tips
- Prefer
to lazily compute expensive data only once per connected session.assign_new/3 - Use
for large lists to minimize diff payloads.stream/3 - Handle params in
for URL-driven state; avoid storing socket state in params.handle_params/3
Live Components
defmodule MyAppWeb.NavComponent do use MyAppWeb, :live_component def render(assigns) do ~H""" <nav> <%= for item <- @items do %> <.link navigate={item.href}><%= item.label %></.link> <% end %> </nav> """ end end
PubSub-driven LiveView
@impl true def mount(_params, _session, socket) do if connected?(socket), do: Phoenix.PubSub.subscribe(MyApp.PubSub, "orders") {:ok, assign(socket, orders: [])} end @impl true def handle_info({:order_created, order}, socket) do {:noreply, update(socket, :orders, fn orders -> [order | orders] end)} end
PubSub, Channels, and Presence
Broadcast changes from contexts
def create_order(attrs) do with {:ok, order} <- %Order{} |> Order.changeset(attrs) |> Repo.insert() do Phoenix.PubSub.broadcast(MyApp.PubSub, "orders", {:order_created, order}) {:ok, order} end end
Presence for online/typing indicators
defmodule MyAppWeb.RoomChannel do use Phoenix.Channel alias Phoenix.Presence def join("room:" <> room_id, _payload, socket) do send(self(), :after_join) {:ok, assign(socket, :room_id, room_id)} end def handle_info(:after_join, socket) do Presence.track(socket, socket.assigns.user_id, %{online_at: System.system_time(:second)}) push(socket, "presence_state", Presence.list(socket)) {:noreply, socket} end end
Security: authorize topics in
join/3, verify user tokens in params/session, and limit payload size.
Testing Phoenix + LiveView
Use
mix test with the generated helpers.
# test/support/conn_case.ex use MyAppWeb.ConnCase, async: true test "renders home", %{conn: conn} do conn = get(conn, "/") assert html_response(conn, 200) =~ "Welcome" end
# LiveView test use MyAppWeb.ConnCase, async: true import Phoenix.LiveViewTest test "counter increments", %{conn: conn} do {:ok, view, _html} = live(conn, "/counter") view |> element("button", "Increment") |> render_click() assert render(view) =~ "Count: 1" end
DataCase: provide sandboxed DB connections; wrap tests in transactions to isolate data.
Fixtures: build factories with
ExMachina or simple helper modules under test/support/fixtures.
Performance, Ops, and Deployment
- Telemetry: Phoenix exposes events (
). Export via[:phoenix, :endpoint, ...]
,:telemetry_poller
, andOpentelemetryPhoenix
.OpentelemetryEcto - Assets:
runs npm install, esbuild, tailwind (if configured), and digests.mix assets.deploy - Releases:
. Configure runtime env inMIX_ENV=prod mix release
. Start withconfig/runtime.exs
.PHX_SERVER=true _build/prod/rel/my_app/bin/my_app start - Clustering: add
with DNS/epmd strategy for horizontal scale; use distributed PubSub/Presence.libcluster - Caching: use ETS/Cachex for hot paths; prefer short TTLs and invalidate on write.
- Background jobs: Oban for retries/backoff; supervise it in application tree.
- Hot path checks: enable
metrics, check LiveView diff sizes, avoid large assigns; prefer streams.:telemetry
Common Pitfalls
- Forgetting to subscribe LiveViews to PubSub after
check — events will be missed on initial render.connected?/1 - Doing heavy work inside LiveView render; move to contexts and precompute assigns.
- Not using
for multi-step writes; failures leave partial state.Ecto.Multi - Blocking BEAM schedulers with long NIFs or heavy CPU work; offload to ports/Oban jobs.
- Overusing global ETS without supervision or limits; leak memory.
Reference Commands
— list routes and LiveView paths.mix phx.routes
— generate LiveView CRUD (review context boundaries afterward).mix phx.gen.live Accounts User users email:string confirmed_at:naive_datetime
— formatting and linting.mix format && mix credo --strict
— deterministic failures; pair withmix test --seed 0 --max-failures 1
.mix test.watch
Phoenix + LiveView excels when domain logic stays in contexts, LiveViews stay thin, and the BEAM supervises every component for resilience.