Use when writing Phoenix/Ecto code. Contains insights about Scopes, Contexts, LiveView lifecycle, and DDD patterns that differ from typical web framework thinking.
/plugin marketplace add georgeguimaraes/claude-code-elixir/plugin install elixir-thinking@claude-code-elixirThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Mental shifts for Phoenix applications. These insights challenge typical web framework patterns.
NO DATABASE QUERIES IN MOUNT
mount/3 is called TWICE (HTTP request + WebSocket connection). Queries in mount = duplicate queries.
The pattern:
mount/3 = setup only (empty assigns, subscriptions, defaults)handle_params/3 = data loading (all database queries)No exceptions:
Scopes address OWASP #1 vulnerability: Broken Access Control.
Before 1.8: You had to remember to scope every query (easy to forget and leak data).
With Scopes: Authorization context is threaded automatically.
# Scope contains user/org for every request
def list_posts(%Scope{user: user}) do
Post |> where(user_id: ^user.id) |> Repo.all()
end
All generators (phx.gen.live, etc.) automatically scope functions.
Critical insight: mount is called TWICE (HTTP + WebSocket).
def mount(_params, _session, socket) do
# NO database queries here! Called twice.
{:ok, assign(socket, posts: [], loading: true)}
end
def handle_params(params, _uri, socket) do
# Database queries here - once per navigation
posts = Blog.list_posts(socket.assigns.scope)
{:noreply, assign(socket, posts: posts, loading: false)}
end
mount/3 = setup (empty assigns, subscriptions) handle_params/3 = data loading (queries, URL-driven state)
Context isn't just a namespace—it changes what words mean:
| Subdomain | What "Product" Means |
|---|---|
| Checkout | SKU, name, description |
| Billing | SKU, quantity, cost per unit |
| Fulfillment | SKU, label, quantity on hand, warehouse |
"The product you thought was a single concept is not the same across subdomains."
Each bounded context may have its OWN Product schema/table.
Listen to domain experts. Same language, different dialects:
Checkout team: "Customer selects products"
Billing team: "We calculate line items"
Fulfillment team: "We ship packages"
Each group uses "product" differently → separate bounded contexts.
Wrong: Entity → Context (database-driven design) Right: Subdomain → Context → Entity
Stop asking "What context does Product belong to?" Start asking "What is a Product in this business domain?"
Apply patterns like Plug conn or Ecto changeset:
def create_product(params) do
params
|> Products.build() # Factory: unstructured → domain
|> Products.validate() # Aggregate: enforce invariants
|> Products.insert() # Repository: persist
end
| Pattern | Elixir Equivalent |
|---|---|
| Entity | Schema with ID |
| Value Object | Embedded schema, struct |
| Aggregate | Module with transactional operations |
| Factory | build/1 functions |
| Repository | get/1, insert/1 wrappers |
Think in events → commands:
| Event (Past) | Command (Context Function) |
|---|---|
| Order form submitted | Checkout.place_order/1 |
| Blog post created | Social.notify_followers/1 |
"Listen to the domain language. Express domain behaviors as functions."
Use events as data structures (not event sourcing machinery):
def agent_joins_room(agent_id) do
with {:ok, room} <- BackgroundCalls.get_available_room(),
{:ok, event} <- BackgroundCalls.start_dialing_session(room, agent_id) do
# Event struct passed to another bounded context
TaskManagement.update_task(event)
end
end
Why events: Minimal data shared, reduces coupling, contexts stay autonomous.
defmodule ShoppingCart.CartItem do
schema "cart_items" do
field :product_id, :integer # Reference by ID
# NOT: belongs_to :product, Catalog.Product
end
end
# Query through the context
def get_cart_with_products(cart) do
product_ids = Enum.map(cart.items, & &1.product_id)
products = Catalog.list_products_by_ids(product_ids)
end
Keeps contexts independent and testable.
Ecto schemas are Elixir representations—they don't have to map 1:1:
| Use Case | Approach |
|---|---|
| Database table | Standard schema/2 |
| Form validation only | embedded_schema/1 |
| API request/response | Embedded schema or schemaless |
| JSON column structure | embeds_one/many |
def registration_changeset(user, attrs) # Full validation + password
def profile_changeset(user, attrs) # Name, bio only
def admin_changeset(user, attrs) # Role, verified_at
Different operations = different changesets.
def subscribe(%Scope{organization: org}) do
Phoenix.PubSub.subscribe(@pubsub, "posts:org:#{org.id}")
end
defp broadcast(%Scope{} = scope, event, payload) do
Phoenix.PubSub.broadcast(@pubsub, topic_for(scope), {event, payload})
end
Unscoped topics = data leaks between tenants.
Bad: Every connected user makes API calls (multiplied by users).
Good: Single GenServer polls, broadcasts to all.
defmodule ExternalDataPoller do
use GenServer
def handle_info(:poll, state) do
data = ExternalAPI.fetch()
Phoenix.PubSub.broadcast(MyApp.PubSub, "external_data", {:update, data})
schedule_poll()
{:noreply, state}
end
end
"Create functional components that depend on assigns from LiveView rather than fetching data inside components."
create table(:comments) do
add :org_id, :integer, null: false
add :post_id, references(:posts, with: [org_id: :org_id], match: :full)
end
Use prepare_query/3 for automatic scoping:
def prepare_query(_operation, query, opts) do
if org_id = opts[:org_id] do
{from(q in query, where: q.org_id == ^org_id), opts}
else
raise "org_id is required"
end
end
"If you have a CRUD bounded context, go for it. No need to add complexity."
Use generators for simple cases. Only add factory/aggregate/repository when business logic demands it.
| Approach | Best For |
|---|---|
| Separate preloads | Has-many with many records (less memory) |
| Join preloads | Belongs-to, has-one (single query) |
Join preloads can use 10x more memory for has-many.
| Excuse | Reality |
|---|---|
| "I'll query in mount, it's simpler" | mount is called twice. Use handle_params. |
| "This app is too small for contexts" | Contexts are about meaning, not size. |
| "I'll just use belongs_to across contexts" | Cross-context = IDs only. Keeps contexts independent. |
| "One schema per table is cleaner" | Multiple schemas per table is valid. Different views = different schemas. |
| "I don't need Scopes for this" | Scopes prevent OWASP #1. Use them. |
| "Preloading everything is easier" | Join preloads can use 10x memory. Think about it. |
| "PubSub topics don't need scoping" | Unscoped topics = data leaks. Always scope. |
| "LiveView can poll the external API" | One GenServer polls, broadcasts to all. Don't multiply requests. |
| "I'll add contexts later" | Refactoring contexts is painful. Design upfront. |
| "CRUD doesn't need DDD" | CRUD contexts are fine. DDD is optional complexity. |
Any of these? Re-read The Iron Law and the relevant section.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.