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
- 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) - 02
ALWAYS use streams for lists >100 items
Regular assigns = O(n) memory per user
- 03
CHECK
connected?/1before PubSub subscribePrevents double subscriptions
- 18
CHECK CHANGESET ERRORS BEFORE UI DEBUGGING
When a form save produces no visible error but no expected side effect, check
{:error, changeset}first - 21
NEVER use
assign_newfor values refreshed every mountassign_newskips the function if the key exists. Useassign/3for locale, current user, or any value that must be set on every mount - 24
MATCH
{:error, %Ecto.Changeset{}}EXPLICITLYBare
{:error, _}merges changeset and non-changeset errors; the form never re-renders validation errors. Handle others separately
Ecto 6 laws
- 04
NEVER use
:floatfor moneyUse
:decimalor:integer(cents) - 05
ALWAYS pin values with
^in queriesNever interpolate user input
- 06
SEPARATE QUERIES for
has_many, JOIN forbelongs_toAvoids row multiplication
- 15
NO IMPLICIT CROSS JOINS
from(a in A, b in B)withouton:creates Cartesian product - 17
DEDUP BEFORE
cast_assocWITH SHARED DATADeduplicate shared child records before building changesets, not inside them
- 19
HIDDEN INPUTS FOR ALL REQUIRED EMBEDDED FIELDS
Every required field in an embedded schema MUST have a
hidden_inputif not directly editable
Oban 3 laws
Security 3 laws
OTP 2 laws
Elixir 4 laws
- 16
@external_resource FOR COMPILE-TIME FILES
Modules reading files at compile time MUST declare
@external_resource - 20
WRAP THIRD-PARTY LIBRARY APIs
Always facade external dependency APIs behind a project-owned module. Enables swapping libraries without touching callers
- 23
MIX TASKS START ONLY WHAT THEY NEED
Mix.Task.run("app.config")+Application.ensure_all_started/1, neverMix.Task.run("app.start")(boots full tree: endpoint port, Oban consuming) - 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
- 22
VERIFY BEFORE CLAIMING DONE
Never say "should work" or "this fixes it." Run
mix compile && mix testand show the result. If you can't verify, explicitly state what remains unverified
Code Style 1 law
- 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