# Elixir

> Use this skill when building with Elixir — Phoenix framework, OTP, BEAM, LiveView, Ecto, supervision trees. This skill enforces: OTP design principles, Phoenix context boundaries, LiveView state management, Ecto schema conventions, supervision tree structure. Do NOT use for: non-Elixir projects, frontend JavaScript, simple scripts better suited for Bash/Python.

- Skill: `j4flmao/elixir` (Agent Skill, multi-file: 7 files)
- Install (CLI): `npx skillmds@latest add j4flmao/elixir`
- Raw SKILL.md: https://api.skillmd.com/api/skills/j4flmao/elixir/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- License: MIT
- Author: j4flmao (https://skillmd.com/u/j4flmao)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/j4flmao/elixir

---


# Elixir

## Purpose
Build fault-tolerant, concurrent applications with Elixir/OTP — Phoenix web layer, LiveView interactivity, Ecto persistence, supervision trees, process architecture.

## Agent Protocol

### Trigger
User request includes: `Elixir`, `Phoenix`, `Phoenix LiveView`, `OTP`, `BEAM`, `Elixir macros`, `Ecto`, `mix`, `phx.gen`, `Supervisor`, `GenServer`, `Phoenix channels`, `Phoenix PubSub`, `Elixir processes`, `iex`.

### Input Context
- Framework (Phoenix, Phoenix LiveView, bare Elixir)
- Persistence (Ecto with PostgreSQL, Ecto with SQLite, ETS)
- State management (GenServer, Agent, ETS, Phoenix PubSub)
- Deployment (Elixir releases, Docker, Gigalixir, Fly.io)

### Output Artifact
Phoenix project structure, OTP supervision tree, Ecto schema, LiveView module, router config.

### Response Format
Produce artifact directly. No preamble, no postamble, no explanations. No filler, no hedging, no transitions. Strip articles a/an/the where unambiguous. Compress output — why use many token when few do trick.

### Completion Criteria
- Phoenix project generated with mix phx.new
- Contexts separated by domain boundary
- Ecto schemas with proper associations
- LiveView with state management and event handling
- Supervision tree with appropriate restart strategy

### Max Response Length
4096 tokens

## Workflow

### Step 1: Phoenix Project Setup

```bash
# Install Phoenix
mix archive.install hex phx_new

# Create new Phoenix app
mix phx.new my_app --database postgres
mix phx.new my_app --database sqlite3  # Alternative

# Setup database
mix ecto.create
mix ecto.migrate

# Start server
mix phx.server
```

```
my_app/
  lib/
    my_app/
      application.ex         # OTP application start
      repo.ex                # Ecto Repo
      accounts/              # Context: accounts
        user.ex              # Ecto schema
        user_notifier.ex     # Boundary call
        user_token.ex
        accounts.ex           # Context module (public API)
      catalog/               # Context: catalog
        product.ex
        category.ex
        catalog.ex
      store/                 # Context: orders
        order.ex
        line_item.ex
        store.ex
      web/
        endpoint.ex           # Phoenix endpoint
        router.ex             # Router
        controllers/          # Controllers (non-LiveView)
          user_session_controller.ex
        live/                 # LiveViews
          product_live/
            index.ex
            show.ex
          cart_live/
            index.ex
        components/           # Shared components
          layout.ex           # App layout
          navbar.ex
          product_card.ex
        templates/            # Templates (non-LiveView)
          layout/
      mailer.ex               # Mailer (Swoosh/Bamboo)
    my_app.ex                 # Module aliases
  priv/
    repo/
      migrations/
        20250101000000_create_users.exs
  config/
    config.exs
    dev.exs
    prod.exs
    runtime.exs
  mix.exs
```

### Step 2: Ecto Schema and Migration

```elixir
# priv/repo/migrations/20250101000000_create_users.exs
defmodule MyApp.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users, primary_key: false) do
      add :id, :uuid, primary_key: true, default: fragment("gen_random_uuid()")
      add :email, :string, null: false
      add :username, :string, null: false
      add :hashed_password, :string, null: false
      add :role, :string, default: "user"
      add :confirmed_at, :naive_datetime
      add :deleted_at, :naive_datetime
      timestamps()
    end

    create unique_index(:users, [:email])
    create unique_index(:users, [:username])
    create index(:users, [:deleted_at])
  end
end
```

```elixir
# 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 :username, :string
    field :role, :string, default: "user"
    field :confirmed_at, :naive_datetime
    field :deleted_at, :naive_datetime

    has_many :orders, MyApp.Store.Order
    has_one :profile, MyApp.Accounts.Profile

    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :username, :role])
    |> validate_required([:email, :username])
    |> validate_format(:email, ~r/@/)
    |> validate_length(:username, min: 3, max: 30)
    |> unique_constraint(:email)
    |> unique_constraint(:username)
  end
end
```

### Step 3: Context Boundary

```elixir
# lib/my_app/accounts/accounts.ex
defmodule MyApp.Accounts do
  @moduledoc """
  Accounts context — user registration, authentication, profile management.
  """
  import Ecto.Query, warn: false
  alias MyApp.Repo
  alias MyApp.Accounts.{User, UserToken, UserNotifier}

  @doc """
  Registers new user.
  """
  def register_user(attrs) do
    %User{}
    |> User.registration_changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Returns user by id.
  """
  def get_user!(id), do: Repo.get!(User, id)

  @doc """
  Authenticates user by email and password.
  """
  def authenticate_by_email(email, password) do
    user = Repo.get_by(User, email: String.downcase(email))

    case check_password(user, password) do
      true -> {:ok, user}
      false -> {:error, :invalid_credentials}
    end
  end

  defp check_password(nil, _password), do: false
  defp check_password(user, password) do
    Argon2.verify_pass(password, user.hashed_password)
  end

  @doc """
  Lists all active users.
  """
  def list_users do
    Repo.all(from u in User, where: is_nil(u.deleted_at), order_by: u.inserted_at)
  end
end
```

### Step 4: Phoenix Router

```elixir
# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, {MyAppWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug MyAppWeb.Plugs.Authenticate
  end

  pipeline :api do
    plug :accepts, ["json"]
    plug MyAppWeb.Plugs.ApiAuth
  end

  # Browser routes
  scope "/", MyAppWeb do
    pipe_through :browser

    get "/", PageController, :index
    get "/login", UserSessionController, :new
    post "/login", UserSessionController, :create
    delete "/logout", UserSessionController, :delete

    live "/products", ProductLive.Index, :index
    live "/products/new", ProductLive.Index, :new
    live "/products/:id/edit", ProductLive.Index, :edit
    live "/products/:id", ProductLive.Show, :show
  end

  # Authenticated routes
  scope "/", MyAppWeb do
    pipe_through [:browser, :require_authenticated]

    live "/dashboard", DashboardLive, :index
    live "/cart", CartLive.Index, :index
    live "/orders", OrderLive.Index, :index
  end

  # API routes
  scope "/api/v1", MyAppWeb do
    pipe_through :api

    post "/users", Api.UserController, :create
    post "/sessions", Api.SessionController, :create
    get "/products", Api.ProductController, :index
  end
end
```

### Step 5: LiveView

```elixir
# lib/my_app_web/live/product_live/index.ex
defmodule MyAppWeb.ProductLive.Index do
  use MyAppWeb, :live_view

  alias MyApp.Catalog
  alias MyApp.Catalog.Product

  @impl true
  def mount(_params, _session, socket) do
    socket =
      socket
      |> assign(:page_title, "Products")
      |> stream(:products, Catalog.list_products())
      |> assign(:form, to_form(%{search: ""}))

    {:ok, socket}
  end

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

  defp apply_action(socket, :edit, %{"id" => id}) do
    socket
    |> assign(:page_title, "Edit Product")
    |> assign(:product, Catalog.get_product!(id))
  end

  defp apply_action(socket, :new, _params) do
    socket
    |> assign(:page_title, "New Product")
    |> assign(:product, %Product{})
  end

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

  @impl true
  def handle_event("search", %{"search" => query}, socket) do
    products = Catalog.search_products(query)
    {:noreply, stream(socket, :products, products, reset: true)}
  end

  @impl true
  def handle_info({MyAppWeb.ProductLive.Index, [:product_updated]}, socket) do
    {:noreply, stream(socket, :products, Catalog.list_products(), reset: true)}
  end
end
```

```elixir
# lib/my_app_web/live/product_live/index.html.heex
<div class="product-list">
  <div class="flex items-center justify-between mb-6">
    <h1 class="text-2xl font-bold">Products</h1>
    <.link navigate={~p"/products/new"}>
      <.button>New Product</.button>
    </.link>
  </div>

  <.form for={@form} phx-change="search" class="mb-4">
    <.input field={@form[:search]} placeholder="Search products..." />
  </.form>

  <.table
    id="products"
    rows={@streams.products}
    row_click={fn {_id, product} -> navigate(~p"/products/#{product}")}
  >
    <:col :let={{_id, product}} label="Name"><%= product.name %></:col>
    <:col :let={{_id, product}} label="Price"><%= product.price %></:col>
    <:col :let={{_id, product}} label="Stock"><%= product.stock_count %></:col>
    <:action :let={{_id, product}}>
      <.link navigate={~p"/products/#{product}/edit"}>Edit</.link>
    </:action>
  </.table>
</div>
```

### Step 6: OTP Supervision Tree

```elixir
# lib/my_app/application.ex
defmodule MyApp.Application do
  @moduledoc false
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Start Ecto repo
      MyApp.Repo,

      # Start Telemetry
      {Phoenix.PubSub, name: MyApp.PubSub},

      # Start Phoenix endpoint
      MyAppWeb.Endpoint,

      # Start workers
      MyApp.Workers.ProductCache,
      MyApp.Workers.EmailQueue,
      MyApp.Workers.SessionCleaner,

      # Start Oban for background jobs
      {Oban, oban_config()},
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end

  defp oban_config do
    Oban.Config.new(
      repo: MyApp.Repo,
      queues: [default: 10, emails: 5, cleanup: 1],
      prune: :active,
    )
  end
end
```

```elixir
# lib/my_app/workers/product_cache.ex
defmodule MyApp.Workers.ProductCache do
  use GenServer

  @cache_ttL :timer.minutes(5)

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end

  @impl true
  def init(state) do
    # Schedule initial cache refresh
    send(self(), :refresh)
    {:ok, state}
  end

  @impl true
  def handle_info(:refresh, state) do
    products = MyApp.Catalog.list_products()
    :ets.new(:product_cache, [:named_table, :public, read_concurrency: true])
    :ets.insert(:product_cache, {:products, products})
    Process.send_after(self(), :refresh, @cache_ttL)
    {:noreply, state}
  end

  def get_products do
    case :ets.lookup(:product_cache, :products) do
      [{:products, products}] -> products
      [] -> MyApp.Catalog.list_products()
    end
  end
end
```

## Rules
- Contexts (Accounts, Catalog, Store) contain all business logic for a domain. Cross-context calls go through public API functions.
- Ecto schemas map to database tables. Changesets handle all validation and casting.
- LiveViews hold state in socket assigns. Phoenix.Component for reusable markup.
- Supervision tree with :one_for_one strategy for workers, :rest_for_one for dependencies.
- GenServer for stateful processes, Agent for simple state, ETS for cache.
- Phoenix.PubSub for cross-process broadcasts (LiveView updates, notifications).
- Mix tasks for one-off jobs. Oban for scheduled/recurring background work.
- Config per environment with runtime.exs for production secrets.

## References
  - references/elixir-advanced.md — Elixir Advanced Topics
  - references/elixir-ecto-advanced.md — Elixir Ecto Advanced Patterns
  - references/elixir-fundamentals.md — Elixir Fundamentals
  - references/elixir-otp.md — Elixir & OTP
  - references/elixir-testing-exunit.md — Elixir Testing with ExUnit
  - references/phoenix-ecto.md — Phoenix & Ecto
## Architecture Decision Trees

### GenServer vs Agent vs ETS
```
State management pattern?
├── Mutable state with side effects → GenServer
│   Confirms that you need state transitions with side effects (sending emails, writing to DB).
│   Use handle_call for synchronous, handle_cast for fire-and-forget.
├── Simple state, no side effects → Agent
│   get/update pattern. No handle_call/cast boilerplate. Good for config, counters.
│   Risk: Agent.update is synchronous — wraps GenServer.call internally.
└── Read-heavy, rarely-changed → ETS
    No process bottleneck. ~1µs reads. Good for reference data, session caches.
    Risk: data lost on restart. Pair with Ecto for durability.
```

### Supervision Strategy Selection
```
Failure recovery requirement?
├── Children independent → :one_for_one
├── Dependency chain → :rest_for_one
│   If DB worker dies, restart DB before workers that depend on it.
├── All must be up → :one_for_all
│   If any child dies, restart all. Use for tightly coupled services.
└── Dynamic children → DynamicSupervisor
    For GenServers started at runtime (e.g., per-user processes).
```

## Implementation Patterns

### Pattern: Phoenix PubSub Broadcast with LiveView Update

```elixir
# lib/my_app/catalog.ex
defmodule MyApp.Catalog do
  alias MyApp.Repo
  alias MyApp.Catalog.Product

  def create_product(attrs) do
    %Product{}
    |> Product.changeset(attrs)
    |> Repo.insert()
    |> broadcast(:product_created)
  end

  defp broadcast({:ok, product}, event) do
    Phoenix.PubSub.broadcast(MyApp.PubSub, "products", {event, product})
    {:ok, product}
  end

  defp broadcast(error, _event), do: error
end

# lib/my_app_web/live/product_live/index.ex (mount)
def mount(_params, _session, socket) do
  if connected?(socket), do: Phoenix.PubSub.subscribe(MyApp.PubSub, "products")
  {:ok, stream(socket, :products, Catalog.list_products())}
end

def handle_info({:product_created, product}, socket) do
  {:noreply, stream_insert(socket, :products, product, at: 0)}
end
```

### Pattern: Ecto Multi for Transactional Operations

```elixir
def place_order(cart_id, customer_id) do
  Ecto.Multi.new()
  |> Ecto.Multi.run(:cart, fn _repo, _changes ->
    case get_cart(cart_id) do
      %Cart{status: :active} = cart -> {:ok, cart}
      _ -> {:error, :cart_not_found}
    end
  end)
  |> Ecto.Multi.run(:order, fn repo, %{cart: cart} ->
    %Order{customer_id: customer_id, total: cart.total}
    |> Order.changeset(%{})
    |> repo.insert()
  end)
  |> Ecto.Multi.run(:line_items, fn repo, %{order: order} ->
    items = Enum.map(cart.items, fn item ->
      %LineItem{order_id: order.id, product_id: item.product_id, quantity: item.quantity, price: item.price}
    end)
    repo.insert_all(LineItem, items)
  end)
  |> Ecto.Multi.run(:clear_cart, fn repo, _changes ->
    repo.delete_all(from c in CartItem, where: c.cart_id == ^cart_id)
  end)
  |> Repo.transaction()
end
```

## Production Considerations

### Deployment Pipeline
```
mix release build → container image → CI tests → staging deploy → smoke tests → prod deploy
```
- Releases: `MIX_ENV=prod mix release` bundles BEAM + deps + config into self-contained directory.
- Database migrations run as separate step BEFORE new release starts. Never auto-migrate on app boot.
- Use `RELEASE_NODE` and `RELEASE_COOKIE` for distributed Erlang node clustering.
- Health checks: `/health` endpoint returns 200 when app is running. DB connectivity checked before startup completes.

### Monitoring & Observability
- Telemetry events: `[:phoenix, :endpoint, :stop]` for request duration, `[:ecto, :query, :total]` for DB.
- Export: TelemetryMetricsPrometheus for `/metrics` endpoint scraped by Prometheus.
- Key metrics: request latency (p50/p95/p99), DB query timing, LiveView socket count, process count, memory.
- Logging: use `Logger.metadata()` with request_id, user_id, tenant_id for structured logs.
- Alert on: process mailbox size > 1000, ETS table growth, Oban queue depth.

## Anti-Patterns

| Anti-Pattern | Why It Hurts | Fix |
|---|---|---|
| GenServer as DB replacement | State lost on crash, restart resets. No durability. | Use Ecto for persistent data; GenServer for cache/state only |
| `IO.inspect` debugging in prod | Compiles into release. Exposes internal state in logs. | `Logger.debug(inspect(data))` removed by compiler in :prod |
| Phoenix context cross-calls | Context A calls Context B's Repo directly. Breaks encapsulation. | Always go through public context API functions |
| Large Ecto preloads in LiveView | 10+ preloads per query = slow mounts. | Paginate, lazy-load, or use ETS cache for reference data |
| `send_after` without cancellation | Timer survives GenServer crash. Fires into dead process. | Store timer ref, cancel on terminate with `Process.cancel_timer/1` |
| `:infinity` GenServer timeout | Blocks caller indefinitely. Cascading failure on stuck call. | Always set explicit timeout (default 5000ms) |

## Performance Optimization

- Streams over Enum for large collections: `Stream.map` / `Stream.filter` avoid intermediate list allocation.
- ETS read concurrency: `:named_table, :public, read_concurrency: true` for read-heavy workloads.
- Ecto query optimization: use `Repo.preload` with `select: [:id, :name]` to fetch only needed columns.
- Phoenix LiveView stream diffing: `stream_insert` / `stream_delete` over full re-render for list performance.
- `IO.iodata_to_binary/1` for string concatenation in hot paths — avoids binary copying.
- `:persistent_term` for truly immutable configuration — fastest read on BEAM (~10ns).
- Oban job batching: batch similar jobs into single worker execution to reduce DB roundtrips.
- Caching with Cachex: `Cachex.fetch(:products, key, fn -> Catalog.get_product(id) end)` with TTL.

## Security Considerations

- Phoenix Router: `:put_secure_browser_headers` plug enabled by default. Don't disable.
- Ecto changesets prevent mass-assignment via `cast/3`. Never use `cast(attrs, [:all])`.
- Password hashing: `Argon2` (memory-hard) over `bcrypt`. Never store plaintext or MD5/SHA.
- Session tokens stored in signed cookies (default). Use database-backed sessions for sensitive apps.
- CORS: configure `Plug.CORS` with explicit origins. Never `Access-Control-Allow-Origin: *` for auth endpoints.
- SQL injection: Ecto's parameterized queries prevent injection. Never use `Ecto.Adapters.SQL.query!` with string interpolation.
- LiveView: validate user permissions in handle_params/handle_event, not just in assigns.
- Rate limiting: use Hammer or ExRated for per-IP/per-user rate limiting on auth endpoints.
- Dependencies: run `mix hex.audit` in CI. Pin versions in mix.lock.

