Rule Writing

CLAUDE.md for Phoenix (Elixir) Framework

Phoenix LiveView changed everything — AI still generates traditional request/response patterns. Rules for LiveView, contexts, Ecto, PubSub, and the BEAM.

8 min read·March 14, 2025

Phoenix LiveView is the default — AI still generates traditional controllers

LiveView, contexts, Ecto changesets, PubSub realtime, and BEAM-aware patterns

Why Phoenix Needs LiveView-First Rules

Phoenix has evolved from a traditional MVC framework into a LiveView-first platform where server-rendered real-time UI is the default. AI assistants generate Phoenix code from the pre-LiveView era: traditional controllers with templates, REST APIs with JSON views, and JavaScript-heavy frontends. Modern Phoenix applications use LiveView for interactivity, PubSub for realtime, and contexts for business logic — AI ignores all three.

The BEAM virtual machine adds another dimension: OTP supervision trees, GenServers for stateful processes, and process-based concurrency are Phoenix's infrastructure layer. AI assistants don't generate BEAM-aware code — they write Phoenix as if it were Rails on a different language, missing the concurrency and fault-tolerance patterns that make BEAM projects resilient.

These rules target Phoenix 1.7+ with LiveView, Ecto, and the standard Phoenix project structure. They complement the Elixir language rules with framework-specific patterns.

Rule 1: LiveView for All Interactive UI

The rule: 'Use Phoenix LiveView for all interactive server-rendered UI. LiveView maintains state on the server and pushes DOM diffs to the client over WebSocket — no JavaScript framework needed. Use live routes: live "/dashboard", DashboardLive. Use live components for reusable interactive UI elements. Use JS hooks (phx-hook) only for browser APIs that LiveView can't access (clipboard, geolocation, third-party widgets).'

For LiveView structure: 'Each LiveView module implements mount/3 (initial state), handle_event/3 (user interactions), handle_info/2 (server-side messages), and render/1 (template). Keep LiveViews focused — one feature per LiveView. Extract reusable UI into live components with live_component/3. Use assigns for state, socket for the connection.'

For realtime: 'Use PubSub for broadcasting updates across LiveView instances: Phoenix.PubSub.broadcast(MyApp.PubSub, "topic", message). Subscribe in mount: Phoenix.PubSub.subscribe(MyApp.PubSub, "topic"). Handle broadcasts in handle_info/2. This gives you real-time multi-user features without any client-side code.'

  • LiveView for all interactive UI — no React, no Vue, no SPA patterns
  • Live routes: live "/path", MyLive — WebSocket-based state management
  • Live components for reusable interactive elements
  • JS hooks only for browser-only APIs — clipboard, geolocation, canvas
  • PubSub for realtime broadcasting across LiveView instances
Realtime for Free

PubSub.broadcast + LiveView handle_info gives you real-time multi-user features with zero client-side code. AI generates WebSocket JavaScript or polling — Phoenix provides realtime natively through the BEAM.

Rule 2: Contexts for Business Logic

The rule: 'Organize business logic into contexts — modules that expose a public API for a domain: Accounts.create_user/1, Catalog.list_products/1, Orders.place_order/2. Contexts are the boundary between the web layer (controllers, LiveViews) and the data layer (Ecto schemas, queries). Never call Ecto.Repo directly from controllers or LiveViews — always go through a context function.'

For context design: 'Each context groups related schemas and operations. Accounts context owns User, Credential, Token schemas. Catalog context owns Product, Category, Variant schemas. Contexts call Repo internally — they're the only modules that touch the database. Controllers and LiveViews call context functions.'

For naming: 'Context functions describe the action: Accounts.register_user/1 (not Accounts.insert_user/1). Return {:ok, result} or {:error, changeset} consistently. Use Ecto.Multi for multi-step transactions within a context. Never leak Ecto schemas outside contexts — return plain maps or structs for the web layer.'

💡 Contexts = API Boundary

Contexts are the wall between web and data. Controllers/LiveViews call Accounts.create_user/1 — never Repo.insert directly. This separation makes business logic testable, reusable, and independent of the web layer.

Rule 3: Ecto Changesets and Schemas

The rule: 'Use Ecto changesets for all data validation and transformation. Define changeset functions on schemas: def changeset(user, attrs) do user |> cast(attrs, [:name, :email]) |> validate_required([:name, :email]) |> validate_format(:email, ~r/@/) |> unique_constraint(:email) end. Use different changeset functions for different operations: registration_changeset, update_changeset, password_changeset.'

For queries: 'Use Ecto.Query for all database queries. Use composable query functions: def active(query) do from u in query, where: u.active == true end. Pipe queries: User |> active() |> recent() |> Repo.all(). Use preload for associations: Repo.all(from u in User, preload: [:orders]). Never use Repo.get! — use Repo.get and handle nil.'

For migrations: 'Use mix ecto.gen.migration for all schema changes. Migrations are forward-only — write reversible migrations with change/0 when possible, up/down when not. Add indexes for foreign keys and frequently queried columns. Use references for foreign keys: add :user_id, references(:users, on_delete: :delete_all).'

  • Changesets for all validation — cast, validate_required, validate_format, unique_constraint
  • Multiple changesets per schema — registration_changeset, update_changeset
  • Composable queries: User |> active() |> recent() |> Repo.all()
  • Preload associations explicitly — never lazy loading (doesn't exist in Ecto)
  • Repo.get (returns nil) over Repo.get! (raises) — handle absence gracefully
⚠️ Repo.get, Not Repo.get!

Repo.get! raises on nil — crashing the process. Repo.get returns nil — letting you handle absence gracefully. In a LiveView, a crash kills the user's connection. Always handle the nil case.

Rule 4: Phoenix Components and HEEx Templates

The rule: 'Use Phoenix function components for all template rendering: def card(assigns) do ~H""" <div class="card"><%= render_slot(@inner_block) %></div> """. Use HEEx templates (.heex) — not EEx (.eex). Use attribute assigns: <.card title={@title}>. Use slots for flexible content: <:header>Title</:header>. Define components in a CoreComponents module for project-wide reuse.'

For the component library: 'Phoenix generates a CoreComponents module with standard components: button, input, table, modal, flash. Customize these — don't create parallel components. Use attr for typed attributes: attr :variant, :string, default: "primary", values: ~w(primary secondary danger). Use slot for content injection.'

For forms: 'Use Phoenix.Component.form/1 with Ecto changesets: <.form for={@form} phx-change="validate" phx-submit="save">. Use <.input> component for form fields. LiveView validates on every change (phx-change) and submits on form submission (phx-submit). Errors display inline from the changeset — no manual error handling.'

Rule 5: BEAM-Aware Patterns

The rule: 'Use OTP patterns where appropriate. Use GenServer for stateful processes: connection pools, caches, rate limiters. Use Supervisor for fault tolerance — every GenServer should be supervised. Use Task for one-off async operations. Use Registry for named process lookup. Use Phoenix.Presence for tracking connected users in realtime features.'

For deployment: 'Use mix release for production releases. Configure runtime.exs for production configuration (not compile-time config). Use environment variables via System.get_env in runtime.exs. Use Phoenix.Endpoint for request handling configuration: port, URL, SSL. Enable clustering with libcluster for multi-node deployments.'

For testing: 'Use ExUnit with Phoenix.ConnTest for controller tests, Phoenix.LiveViewTest for LiveView tests. Use Ecto.Adapters.SQL.Sandbox for database isolation in tests. Use Mox for mocking — define behaviours for mockable modules. Test contexts directly — they're the most important unit to test. Test LiveViews by simulating user events: render_click, render_change, render_submit.'

Complete Phoenix Rules Template

Consolidated rules for Phoenix 1.7+ projects (use alongside Elixir language rules).

  • LiveView for all interactive UI — PubSub for realtime — JS hooks only for browser APIs
  • Contexts for business logic: public API boundary between web and data layers
  • Ecto changesets for all validation — composable queries — explicit preloads
  • HEEx templates with function components — CoreComponents for project-wide UI
  • Forms with changesets: phx-change for validation, phx-submit for submission
  • GenServer for stateful processes — Supervisor for fault tolerance — Task for async
  • mix release for production — runtime.exs for config — libcluster for multi-node
  • ExUnit + ConnTest + LiveViewTest — SQL.Sandbox — Mox for mocking