From atum-stack-backend
Elixir / Phoenix / OTP pattern library — Elixir 1.16+ functional language on the BEAM virtual machine (Erlang VM) for fault-tolerant distributed systems, OTP behaviours (GenServer for stateful processes, Supervisor for fault tolerance trees, Application for app structure, Task for async work, Agent for simple state, Registry for process discovery), Phoenix 1.7+ web framework (router, controllers, contexts as bounded subdomains, Ecto for database with multi schema + multi tenant + transactions + multi step transactions, channels for WebSocket pub/sub, presence for online tracking), Phoenix LiveView (server-rendered reactive UI without JavaScript, replacing SPAs for many use cases, with hooks for client-side interop and JS commands), Phoenix Components + Function Components, Tailwind + Heroicons integration, AshFramework for declarative resources (alternative to Ecto contexts), Broadway for data ingestion pipelines, Oban for background jobs (Postgres-backed, replacing Sidekiq for Elixir), Tesla for HTTP clients with middleware, mix tasks + releases for production deployment, distillery legacy vs mix release modern, hot code upgrades, telemetry + telemetry_metrics + telemetry_poller for observability, ExUnit for testing with property-based PropCheck / StreamData, Dialyzer for static analysis, and the Tidewave MCP server for Phoenix introspection by Claude Code. Use when developing real-time applications, fault-tolerant systems, IoT backends, chat / collab tools, or anything that benefits from the BEAM VM model. Differentiates from generic backend patterns by Elixir-specific concurrency model (lightweight processes, message passing) and Phoenix LiveView server-rendered reactive paradigm.
npx claudepluginhub arnwaldn/atum-plugins-collection --plugin atum-stack-backendThis skill uses the workspace's default tool permissions.
Patterns canoniques pour construire des **applications web temps-réel et fault-tolerant** avec Elixir + Phoenix + OTP.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides implementation of event-driven hooks in Claude Code plugins using prompt-based validation and bash commands for PreToolUse, Stop, and session events.
Patterns canoniques pour construire des applications web temps-réel et fault-tolerant avec Elixir + Phoenix + OTP.
| Use case | Elixir/Phoenix vs alternative |
|---|---|
| Chat / collab temps-réel | Phoenix LiveView + Channels + Presence > Node.js + Socket.IO |
| Backend IoT massivement concurrent | BEAM VM lightweight processes > tout |
| Fault tolerance distribué | OTP supervisors > custom retry/circuit-breaker |
| Server-rendered SPA-like UX | LiveView > React + WebSocket sync custom |
| Streaming data pipelines | GenStage + Broadway > Apache Kafka Streams |
| Soft real-time gaming backend | BEAM > Node/Go (1M+ connections par node) |
| Apps simples CRUD | Postgres + Rails / Django > Phoenix (overkill) |
Règle : Phoenix brille quand tu as besoin de temps-réel + fault tolerance + concurrence massive simultanément. Pour du CRUD basique, Rails/Django sont plus rapides à développer.
# Install Elixir + Erlang
brew install elixir
# Install Phoenix
mix archive.install hex phx_new
# Create project
mix phx.new my_app --database postgres --live # avec LiveView
cd my_app
mix ecto.create
mix phx.server
Structure :
my_app/
├── config/ # config par env
├── lib/
│ ├── my_app/ # business logic (contexts)
│ │ ├── accounts/ # context: user accounts
│ │ │ ├── user.ex # Ecto schema
│ │ │ └── ...
│ │ ├── catalog/ # context: products
│ │ └── application.ex # OTP application + supervision tree
│ └── my_app_web/ # web layer (router, controllers, views)
│ ├── controllers/
│ ├── components/
│ ├── live/ # LiveView pages
│ ├── router.ex
│ └── endpoint.ex
├── priv/repo/migrations/ # DB migrations
├── test/
└── mix.exs
# lib/my_app/cache_server.ex
defmodule MyApp.CacheServer do
use GenServer
# Client API
def start_link(opts) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def get(key), do: GenServer.call(__MODULE__, {:get, key})
def put(key, value), do: GenServer.cast(__MODULE__, {:put, key, value})
# Server callbacks
@impl true
def init(state), do: {:ok, state}
@impl true
def handle_call({:get, key}, _from, state) do
{:reply, Map.get(state, key), state}
end
@impl true
def handle_cast({:put, key, value}, state) do
{:noreply, Map.put(state, key, value)}
end
end
# lib/my_app/application.ex
defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
MyApp.CacheServer, # ajouté à la supervision tree
{Phoenix.PubSub, name: MyApp.PubSub},
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
Si CacheServer crash, le supervisor le restart automatiquement → "Let it crash" philosophy.
# lib/my_app/accounts.ex
defmodule MyApp.Accounts do
alias MyApp.Repo
alias MyApp.Accounts.User
def list_users, do: Repo.all(User)
def get_user!(id), do: Repo.get!(User, id)
def create_user(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
def update_user(%User{} = user, attrs) do
user
|> User.changeset(attrs)
|> Repo.update()
end
def delete_user(%User{} = user), do: Repo.delete(user)
end
Le context est le seul point d'entrée pour les opérations sur ce domaine. Le controller appelle MyApp.Accounts.create_user(params), il ne touche pas au schema directement.
# lib/my_app_web/live/counter_live.ex
defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
@impl true
def handle_event("increment", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
@impl true
def render(assigns) do
~H"""
<div class="p-4">
<h1 class="text-2xl">Count: <%= @count %></h1>
<button phx-click="increment" class="bg-blue-500 text-white px-4 py-2 rounded">
+1
</button>
</div>
"""
end
end
Tout ce code tourne côté serveur. Quand l'user clique, Phoenix envoie l'event via WebSocket, met à jour @count, re-render le HTML, et envoie le diff au browser. Zéro ligne de JS écrite par l'utilisateur.
LiveView est plus simple et souvent plus performant qu'une SPA React pour 80% des use cases.
# lib/my_app_web/channels/room_channel.ex
defmodule MyAppWeb.RoomChannel do
use MyAppWeb, :channel
@impl true
def join("room:" <> room_id, _params, socket) do
{:ok, assign(socket, :room_id, room_id)}
end
@impl true
def handle_in("new_message", %{"body" => body}, socket) do
broadcast!(socket, "new_message", %{body: body, user: socket.assigns.user_id})
{:reply, :ok, socket}
end
end
Côté JS client (le seul endroit où on a besoin d'un peu de JS) :
import { Socket } from "phoenix"
const socket = new Socket("/socket", { params: { token: jwt } })
socket.connect()
const channel = socket.channel("room:42", {})
channel.join()
channel.on("new_message", payload => console.log(payload))
channel.push("new_message", { body: "Hello" })
# priv/repo/migrations/20260408_create_users.exs
defmodule MyApp.Repo.Migrations.CreateUsers do
use Ecto.Migration
def change do
create table(:users) do
add :email, :string, null: false
add :name, :string, null: false
timestamps()
end
create unique_index(:users, [:email])
end
end
# Multi-step transaction
alias Ecto.Multi
result =
Multi.new()
|> Multi.insert(:user, User.changeset(%User{}, params))
|> Multi.insert(:profile, fn %{user: user} ->
Profile.changeset(%Profile{user_id: user.id}, %{})
end)
|> Multi.run(:welcome_email, fn _repo, %{user: user} ->
MyApp.Mailer.send_welcome(user)
end)
|> MyApp.Repo.transaction()
Si n'importe quelle étape échoue, toute la transaction est rollback.
mix deps.get oban
# lib/my_app/workers/email_worker.ex
defmodule MyApp.Workers.EmailWorker do
use Oban.Worker, queue: :emails, max_attempts: 5
@impl Oban.Worker
def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
user = MyApp.Accounts.get_user!(user_id)
MyApp.Mailer.send_welcome(user)
end
end
# Enqueue
%{user_id: user.id}
|> MyApp.Workers.EmailWorker.new()
|> Oban.insert()
Oban stocke les jobs dans Postgres (pas Redis), ce qui simplifie la stack et donne des features advanced (cron jobs, unique jobs, recurring, retries).
mix release
# → _build/prod/rel/my_app/bin/my_app start
Une release Elixir contient :
Pas besoin d'Elixir installé sur le serveur cible — la release est self-contained.
# Sur le serveur prod
./bin/my_app eval "MyApp.Release.migrate()"
./bin/my_app start
join/3 — accès libre aux roomsRepo.preload([:relation])mix release — install Elixir partout en prodapi-designer (ce plugin)realtime-websocket (ce plugin)deploy-railway ou deploy-fly (atum-workflows)property-based-testing (ce plugin)