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
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 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! 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