Back to Skills

phoenix-thinking

verified

This skill should be used when the user asks to "add a LiveView page", "create a form", "handle real-time updates", "broadcast changes to users", "add a new route", "create an API endpoint", "fix this LiveView bug", "why is mount called twice?", or mentions handle_event, handle_info, handle_params, mount, channels, controllers, components, assigns, sockets, or PubSub. Essential for avoiding duplicate queries in mount.

View on GitHub

Marketplace

claude-code-elixir

georgeguimaraes/claude-code-elixir

Plugin

elixir

skills

Repository

georgeguimaraes/claude-code-elixir
80stars

plugins/elixir/skills/phoenix-thinking/SKILL.md

Last Verified

January 23, 2026

Install Skill

Select agents to install to:

Scope:
npx add-skill https://github.com/georgeguimaraes/claude-code-elixir/blob/main/plugins/elixir/skills/phoenix-thinking/SKILL.md -a claude-code --skill phoenix-thinking

Installation paths:

Claude
.claude/skills/phoenix-thinking/
Powered by add-skill CLI

Instructions

# Phoenix Thinking

Mental shifts for Phoenix applications. These insights challenge typical web framework patterns.

## The Iron Law

```
NO DATABASE QUERIES IN MOUNT
```

mount/3 is called TWICE (HTTP request + WebSocket connection). Queries in mount = duplicate queries.

```elixir
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 only (empty assigns, subscriptions, defaults)
**handle_params/3** = data loading (all database queries, URL-driven state)

**No exceptions:** Don't query "just this one small thing" in mount. Don't "optimize later". LiveView lifecycle is non-negotiable.

## Scopes: Security-First Pattern (Phoenix 1.8+)

Scopes address OWASP #1 vulnerability: Broken Access Control. Authorization context is threaded automatically—no more forgetting to scope queries.

```elixir
def list_posts(%Scope{user: user}) do
  Post |> where(user_id: ^user.id) |> Repo.all()
end
```

## PubSub Topics Must Be Scoped

```elixir
def subscribe(%Scope{organization: org}) do
  Phoenix.PubSub.subscribe(@pubsub, "posts:org:#{org.id}")
end
```

Unscoped topics = data leaks between tenants.

## External Polling: GenServer, Not LiveView

**Bad:** Every connected user makes API calls (multiplied by users).
**Good:** Single GenServer polls, broadcasts to all via PubSub.

## Components Receive Data, LiveViews Own Data

- **Functional components:** Display-only, no internal state
- **LiveComponents:** Own state, handle own events
- **LiveViews:** Full page, owns URL, top-level state

## Async Data Loading

Use `assign_async/3` for data that can load after mount:

```elixir
def mount(_params, _session, socket) do
  {:ok, assign_async(socket, :user, fn -> {:ok, %{user: fetch_

Validation Details

Front Matter
Required Fields
Valid Name Format
Valid Description
Has Sections
Allowed Tools
Instruction Length:
4183 chars