Iron Laws

26 non-negotiable rules extracted from production Phoenix codebases. Each rule prevents a class of bugs that tests rarely catch. When code would violate a law, the plugin stops and explains before proceeding.

Defined in CLAUDE.md in the plugin repo and inlined into every relevant skill.

LiveView 6 laws

  1. 01

    NO unconditional DB queries in mount

    Mount runs twice. Default: assign_async. SEO routes: connected? + cache-backed disconnected branch (dead-render IS the crawler-indexed HTML)

  2. 02

    ALWAYS use streams for lists >100 items

    Regular assigns = O(n) memory per user

  3. 03

    CHECK connected?/1 before PubSub subscribe

    Prevents double subscriptions

  4. 18

    CHECK CHANGESET ERRORS BEFORE UI DEBUGGING

    When a form save produces no visible error but no expected side effect, check {:error, changeset} first

  5. 21

    NEVER use assign_new for values refreshed every mount

    assign_new skips the function if the key exists. Use assign/3 for locale, current user, or any value that must be set on every mount

  6. 24

    MATCH {:error, %Ecto.Changeset{}} EXPLICITLY

    Bare {:error, _} merges changeset and non-changeset errors; the form never re-renders validation errors. Handle others separately

Ecto 6 laws

  1. 04

    NEVER use :float for money

    Use :decimal or :integer (cents)

  2. 05

    ALWAYS pin values with ^ in queries

    Never interpolate user input

  3. 06

    SEPARATE QUERIES for has_many, JOIN for belongs_to

    Avoids row multiplication

  4. 15

    NO IMPLICIT CROSS JOINS

    from(a in A, b in B) without on: creates Cartesian product

  5. 17

    DEDUP BEFORE cast_assoc WITH SHARED DATA

    Deduplicate shared child records before building changesets, not inside them

  6. 19

    HIDDEN INPUTS FOR ALL REQUIRED EMBEDDED FIELDS

    Every required field in an embedded schema MUST have a hidden_input if not directly editable

Oban 3 laws

  1. 07

    Jobs MUST be idempotent

    Safe to retry

  2. 08

    Args use STRING keys, not atoms

    Pattern match %{"user_id" => id}

  3. 09

    NEVER store structs in args

    Store IDs, not %User{}

Security 3 laws

  1. 10

    NO String.to_atom with user input

    Atom exhaustion DoS

  2. 11

    AUTHORIZE in EVERY LiveView handle_event

    Don't trust mount authorization

  3. 12

    NEVER use raw/1 with untrusted content

    XSS vulnerability

OTP 2 laws

  1. 13

    NO process without runtime reason

    Processes model concurrency/state/isolation, NOT code structure

  2. 14

    SUPERVISE ALL LONG-LIVED PROCESSES

    Never bare GenServer.start_link/Agent.start_link in production. Use supervision trees

Elixir 4 laws

  1. 16

    @external_resource FOR COMPILE-TIME FILES

    Modules reading files at compile time MUST declare @external_resource

  2. 20

    WRAP THIRD-PARTY LIBRARY APIs

    Always facade external dependency APIs behind a project-owned module. Enables swapping libraries without touching callers

  3. 23

    MIX TASKS START ONLY WHAT THEY NEED

    Mix.Task.run("app.config") + Application.ensure_all_started/1, never Mix.Task.run("app.start") (boots full tree: endpoint port, Oban consuming)

  4. 25

    CAPTURE LOCALE BEFORE SPAWNING

    Gettext/CLDR locale is process-local. Read it in the caller and pass it explicitly — a spawned Task/GenServer starts with the default locale

Verification 1 law

  1. 22

    VERIFY BEFORE CLAIMING DONE

    Never say "should work" or "this fixes it." Run mix compile && mix test and show the result. If you can't verify, explicitly state what remains unverified

Code Style 1 law

  1. 26

    COMMENTS AREN'T COMMIT MESSAGES

    A change's reasoning belongs in the commit/PR — git persists it, not code. No issue tags inline. Keep only durable facts: footguns, invariants, quirks