phoenix-pubsub-patterns
Skill4 starsChanged 4 months agoMITView on GitHub
MANDATORY for ALL PubSub and real-time broadcast work. Invoke before writing PubSub.subscribe, broadcast, or handle_info for real-time updates. Covers subscription patterns, broadcasting from contexts, topic naming, scoped broadcasting, immutable assign updates, and testing. Trigger words: PubSub, subscribe, broadcast, handle_info, real-time, topic, presence.
In this repository34 other files it gives its agents.
- AGENTS.mdAGENTS.md
- apply-phoenix-controller-conventionsSkill
- ash-frameworkSkill
- benchee-profilingSkill
- broadway-data-pipelinesSkill
- cachex-cachingSkill
- code-qualitySkill
- code-reviewSkill
- deployment-gotchasSkill
- ecto-changeset-patternsSkill
- ecto-essentialsSkill
- ecto-migrationSkill
- ecto-nested-associationsSkill
- elixir-essentialsSkill
- gettext-i18nSkill
- liveview-streamsSkill
- mix-tasks-generatorsSkill
- oban-essentialsSkill
- otp-essentialsSkill
- phoenix-authenticationSkill
- phoenix-authorization-patternsSkill
- phoenix-channels-essentialsSkill
- phoenix-json-apiSkill
- phoenix-liveview-essentialsSkill
- phoenix-uploadsSkill
- property-based-testingSkill
- refactor-codeSkill
- req-http-clientSkill
- respond-to-reviewSkill
- security-essentialsSkill
- swoosh-emailsSkill
- telemetry-essentialsSkill
- testing-essentialsSkill
- typespec-dialyzerSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 18
Use this skill before writing ANY PubSub or real-time broadcast code.
- Very specific: line 128
Test by calling context functions and asserting the LiveView reflects the update — do not test PubSub.broadcast in isolation.
- Very specific: line 159
Subscription not firing?: Verify the LiveView is fully connected: subscriptions inside if connected?(socket) only run after WebSocket …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Phoenix PubSub Patterns
Use this skill before writing ANY PubSub or real-time broadcast code.
RULES — Follow these with no exceptions
1. Subscribe inside if connected?(socket) — never subscribe on the static render, or the disconnected and connected phases both subscribe and you get duplicate messages
2. Broadcast from context modules, not LiveViews — keep real-time logic in the business layer
3. Only broadcast on success — pattern-match {:ok, result} in a private broadcast/2 and pass {:error, changeset} through untouched
4. Update assigns immutably with update/3 in handle_info/2 — never mutate socket.assigns
5. Match subscribe and broadcast topic strings exactly — topics are case-sensitive and must be identical
6. Add a handle_info/2 clause for every broadcast event — an unhandled message crashes the LiveView; add a catch-all when other processes may send messages
7. Test the full cycle through the LiveView — call the context function and assert the rendered view updates; don't test PubSub.broadcast in isolation
Implementation Workflow
- Subscribe in
mount— guard withif connected?(socket)to prevent duplicate subscriptions - Broadcast from context — add a private
broadcast/2helper that fires only on{:ok, result} - Handle in
handle_info/2— update assigns immutably withupdate/3 - Verify with a test — call the context function and assert the LiveView reflects the change
Subscription Pattern
defmodule MyAppWeb.PostLive.Index do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "posts")
end
{:ok, assign(socket, :posts, list_posts())}
end
@impl true
def handle_info({:post_created, post}, socket) do
{:noreply, update(socket, :posts, fn posts -> [post | posts] end)}
end
@impl true
def handle_info({:post_updated, post}, socket) do
{:noreply,
update(socket, :posts, fn posts ->
Enum.map(posts, fn
p when p.id == post.id -> post
p -> p
end)
end)}
end
@impl true
def handle_info({:post_deleted, post}, socket) do
{:noreply,
update(socket, :posts, fn posts ->
Enum.reject(posts, &(&1.id == post.id))
end)}
end
endBroadcasting from Contexts
Broadcast from contexts, not LiveViews — keeps real-time logic in the business layer. Topic naming conventions:
"posts"— collection-wide; events:{:post_created, post},{:post_updated, post},{:post_deleted, post}"posts:#{post.id}"— specific resource; events:{:post_updated, post},{:comment_added, comment}"users:#{user.id}"— user-scoped; events:{:notification, notification},{:message_received, message}
defmodule MyApp.Blog do
def create_post(attrs) do
%Post{}
|> Post.changeset(attrs)
|> Repo.insert()
|> broadcast(:post_created)
end
def update_post(%Post{} = post, attrs) do
post
|> Post.changeset(attrs)
|> Repo.update()
|> broadcast(:post_updated)
end
def delete_post(%Post{} = post) do
post
|> Repo.delete()
|> broadcast(:post_deleted)
end
# Only broadcast on success
defp broadcast({:ok, post}, event) do
Phoenix.PubSub.broadcast(MyApp.PubSub, "posts", {event, post})
{:ok, post}
end
defp broadcast({:error, changeset}, _event) do
{:error, changeset}
end
endTesting the Full PubSub Cycle
Test by calling context functions and asserting the LiveView reflects the update — do not test PubSub.broadcast in isolation.
defmodule MyAppWeb.PostLive.IndexTest do
use MyAppWeb.ConnCase, async: true
import Phoenix.LiveViewTest
test "creates a post and LiveView updates in real time", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/posts")
# Call the context function — it broadcasts internally
{:ok, post} = MyApp.Blog.create_post(%{title: "Hello", body: "World"})
# Assert the LiveView received and rendered the broadcast
assert render(view) =~ post.title
end
test "deletes a post and LiveView removes it", %{conn: conn} do
post = insert(:post)
{:ok, view, _html} = live(conn, ~p"/posts")
{:ok, _} = MyApp.Blog.delete_post(post)
refute render(view) =~ post.title
end
endTroubleshooting / Validation Checkpoints
- Subscription not firing? Verify the LiveView is fully connected: subscriptions inside
if connected?(socket)only run after WebSocket upgrade, not on the initial static render. - Broadcast sent but LiveView not updating? Confirm the topic string in
subscribeandbroadcastmatch exactly (case-sensitive). Add a temporaryIO.inspectinhandle_info/2to confirm the message is arriving. - Duplicate messages? You subscribed outside the
if connected?(socket)guard — the static render and the live render both subscribed. handle_infoclause missing? An unhandled PubSub message will crash the LiveView process. Add a catch-alldef handle_info(_, socket), do: {:noreply, socket}if other processes may send unexpected messages.
Common Pitfalls
| ❌ Don't | ✅ Do |
Subscribe outside the connected? guard | Subscribe only inside if connected?(socket) |
| Broadcast directly from a LiveView | Broadcast from the context after a successful write |
| Broadcast before checking the result | Broadcast only on {:ok, result}; pass errors through |
Mismatch subscribe/broadcast topic strings | Use identical, case-sensitive topic strings |
Mutate socket.assigns in handle_info/2 | Update immutably with update/3 |
| Leave a broadcast event without a matching clause | Add a handle_info/2 clause (plus a catch-all) |
Assert on PubSub.broadcast in isolation | Drive the context function and assert the view updates |
Integration
| Predecessor | This Skill | Successor |
| phoenix-liveview-essentials | phoenix-pubsub-patterns | testing-essentials |
| liveview-streams | phoenix-pubsub-patterns | phoenix-channels-essentials |
Companion skills:
phoenix-liveview-essentials— LiveView lifecycle that receives the broadcastsliveview-streams— apply broadcasts as targeted stream inserts/deletesphoenix-channels-essentials— broadcasting to non-LiveView clients
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

