Elixir Phoenix Agent Rules
Project Context
You are building Phoenix 1.7+ applications with Elixir. Organize business logic in contexts, use Ecto for database access, and leverage LiveView for interactive UIs. Follow Phoenix conventions for a codebase that scales with the team.
Code Style
- Write pure functions wherever possible; push side effects to context boundaries and controllers.
- Use the pipe operator `|>` for data transformation chains.
- Use pattern matching in function heads for control flow; use `with` for multi-step fallible operations.
- Write `@doc` and `@spec` for all public context functions.
- Keep functions short (under 15 lines). Extract private helpers with `defp`.
Project Structure
- Organize business logic into contexts: `MyApp.Accounts`, `MyApp.Catalog`, `MyApp.Orders`.
- Each context exposes a public API of named functions; schemas and queries are internal to the context.
- Place web-specific code in `MyAppWeb`: controllers, live views, components, router.
- Controllers and LiveViews call context functions only — never call `Repo` directly from web layer code.
- Separate schema modules (`MyApp.Orders.Order`) from context modules (`MyApp.Orders`).
Controllers
- Keep controllers thin: parse params, call context function, render or redirect.
- Use `conn |> put_status(:created) |> json(order)` for RESTful API responses.
- Use `action_fallback MyAppWeb.FallbackController` to centralize error handling across controllers.
- Define `FallbackController` to pattern match on `{:error, reason}` tuples and return appropriate responses.
- Use `require_authenticated_user` plug for protected routes.
Ecto
- Write changesets for every data mutation — never insert or update without validating through a changeset.
- Use `validate_required`, `validate_format`, `validate_length`, `unique_constraint` in changesets.
- Use `Ecto.Multi` for operations spanning multiple database writes: `Multi.new() |> Multi.insert(...) |> Multi.update(...) |> Repo.transaction()`.
- Use `from(o in Order, where: o.status == ^:pending, order_by: [desc: o.inserted_at])` with explicit bindings.
- Add indexes in migrations with `create index(:orders, [:user_id])` and unique indexes with `create unique_index`.
- Write reversible migrations using `up/0` and `down/0` functions.
GenServer & OTP
- Use `GenServer` for stateful processes: caches, rate limiters, connection pools.
- Expose a public API module wrapping `GenServer.call/cast`: `def get_rate(limiter, key), do: GenServer.call(limiter, {:get, key})`.
- Use `Supervisor.child_spec` to configure restart strategies explicitly.
- Use `Registry` for dynamically named workers: `{:via, Registry, {MyApp.Registry, user_id}}`.
LiveView
- Use `mount/3` for initial data loading; use `handle_params/3` for URL-driven state.
- Keep socket assigns minimal — store only what the template needs to render.
- Use `handle_event/3` for user interactions; return `{:noreply, socket}` for updates, `{:reply, payload, socket}` for push replies.
- Use Phoenix LiveView streams (`stream/3`, `stream_insert/3`) for large, dynamic lists.
- Use `push_patch` for navigation that updates URL params without a full remount.
Error Handling
- Return `{:ok, value}` / `{:error, reason}` from all context functions.
- Match on error tuples in `with` chains; handle them in the `else` clause.
- Use `FallbackController` to map `{:error, %Ecto.Changeset{}}` to `422 Unprocessable Entity`.
- Log errors with `Logger.error("Operation failed", metadata: [user_id: id, reason: inspect(reason)])`.
Testing
- Write tests with ExUnit; use `async: true` on modules that do not share database state.
- Use `Ecto.Adapters.SQL.Sandbox` for database tests with automatic rollback after each test case.
- Test context functions directly: `assert {:ok, order} = Orders.create_order(valid_attrs)`.
- Test LiveViews with `Phoenix.LiveViewTest`: `{:ok, view, html} = live(conn, "/orders")`.
- Use `Mox` for external service mocks; define behavior modules and set expectations per test.
Deployment
- Use `mix release` for production releases; configure secrets in `runtime.exs` via `System.get_env`.
- Run migrations on deploy with `bin/app eval "MyApp.Release.migrate()"` — not at application startup.
- Use Oban for persistent background jobs with retries, scheduling, and observability.
- Implement `/healthz` endpoint that checks database connectivity and responds within 200ms.