LEARN PHOENIX FRAMEWORK DEEP DIVE
Learn Phoenix Framework: From BEAM Fundamentals to Real-Time Mastery
Goal: Deeply understand Phoenix—not just how to use it, but how it works internally, why it’s so fast, what makes LiveView magical, and how it compares to other web frameworks. You’ll build everything from a raw TCP server to a full real-time application, understanding each layer.
Why Phoenix Matters
Phoenix isn’t just another web framework. It’s built on Elixir, which runs on the BEAM (Erlang Virtual Machine)—the same technology that powers WhatsApp (handling 2 million connections per server), Discord (handling 5 million concurrent users), and telecom systems with 99.9999999% uptime.
After completing these projects, you will:
- Understand the BEAM’s actor model and why it enables massive concurrency
- Know how Phoenix handles millions of WebSocket connections
- Understand how LiveView creates reactive UIs without JavaScript
- See how Plug middleware composes the request pipeline
- Master Ecto’s functional approach to database interactions
- Compare Phoenix to Rails, Django, Express, and Go frameworks
- Build real-time applications that scale horizontally
Core Concept Analysis
The Technology Stack
┌─────────────────────────────────────────────────────────────────────────┐
│ YOUR APPLICATION │
├─────────────────────────────────────────────────────────────────────────┤
│ PHOENIX FRAMEWORK │
│ (Router, Controllers, Views, Channels, LiveView) │
├─────────────────────────────────────────────────────────────────────────┤
│ PLUG │
│ (Composable middleware, Conn struct) │
├─────────────────────────────────────────────────────────────────────────┤
│ ECTO │
│ (Database toolkit: Repos, Schemas, Changesets, Query) │
├─────────────────────────────────────────────────────────────────────────┤
│ COWBOY HTTP SERVER │
│ (Erlang HTTP/1.1, HTTP/2, WebSocket server) │
├─────────────────────────────────────────────────────────────────────────┤
│ ELIXIR LANGUAGE │
│ (Functional, immutable, pattern matching, metaprogramming) │
├─────────────────────────────────────────────────────────────────────────┤
│ OTP (Open Telecom Platform) │
│ (GenServer, Supervisor, Application - behaviors for concurrency) │
├─────────────────────────────────────────────────────────────────────────┤
│ BEAM Virtual Machine │
│ (Lightweight processes, preemptive scheduling, fault tolerance) │
└─────────────────────────────────────────────────────────────────────────┘
The BEAM’s Secret Sauce: Lightweight Processes
Traditional Web Server (Node.js, Python, Ruby)
──────────────────────────────────────────────
┌───────────────────────────────────────────────────────────┐
│ Single OS Thread/Process │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Request 1 ───► Request 2 ───► Request 3 ───► ... │ │
│ │ (blocking or callback-based async) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Problem: One slow request can affect others │
│ Problem: Crash in one request can crash everything │
└───────────────────────────────────────────────────────────┘
BEAM Virtual Machine (Elixir/Phoenix)
─────────────────────────────────────
┌─────────────────────────────────────────────────────────────────────┐
│ BEAM VM (per CPU core) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Scheduler 1 Scheduler 2 Scheduler 3 ... │ │
│ │ │ │ │ │ │
│ │ ┌───┴───┐ ┌───┴───┐ ┌───┴───┐ │ │
│ │ │ P P │ │ P P │ │ P P │ │ │
│ │ │ P P │ │ P P │ │ P P │ │ │
│ │ │ P P │ │ P P │ │ P P │ │ │
│ │ └───────┘ └───────┘ └───────┘ │ │
│ │ (Each P is a lightweight process, ~2KB memory) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ✓ Each request gets its own isolated process │
│ ✓ Processes are preemptively scheduled (no blocking) │
│ ✓ Crash in one process doesn't affect others │
│ ✓ Millions of processes can run concurrently │
└─────────────────────────────────────────────────────────────────────┘
Phoenix Request Flow
HTTP Request
│
▼
┌─────────────┐
│ Endpoint │ ← First stop: static files, logging, session, CSRF
│ (Plug) │
└─────┬───────┘
│
▼
┌─────────────┐
│ Router │ ← Matches URL pattern, selects pipeline and controller
│ (Plug) │
└─────┬───────┘
│
▼
┌─────────────┐
│ Pipeline │ ← :browser adds session, flash, etc.
│ (Plugs) │ :api adds JSON parsing
└─────┬───────┘
│
▼
┌─────────────┐
│ Controller │ ← Handles business logic, calls context functions
│ (Plug) │
└─────┬───────┘
│
▼
┌─────────────┐
│ View │ ← Prepares data for rendering
│ │
└─────┬───────┘
│
▼
┌─────────────┐
│ Template │ ← Compiled EEx, generates HTML
│ (HEEx) │
└─────┬───────┘
│
▼
HTTP Response
LiveView: How It Works
Initial Page Load (HTTP)
────────────────────────
Browser ──HTTP GET──► Phoenix ──renders──► Full HTML Page
│
▼
Browser displays page
│
▼
JavaScript loads
│
▼
WebSocket connects
│
▼
┌────────────────┴────────────────┐
│ │
Phoenix spawns Browser ready
LiveView process for updates
(stateful, long-lived)
User Interaction (WebSocket)
────────────────────────────
Browser LiveView Process
│ │
│──── phx-click="increment" ────────────────────────►│
│ │
│ handle_event("increment")
│ │
│ Updates socket.assigns
│ │
│ Re-renders template
│ │
│ Computes DIFF (only changes)
│ │
│◄───────── {diff: [{0: "6"}]} ─────────────────────│
│ │
│ JavaScript patches DOM │
│ with minimal changes │
▼ │
Key Insight: Only the changed parts are sent!
- Template has static and dynamic parts
- Phoenix tracks which assigns changed
- Only sends new values for changed dynamics
How Phoenix Compares to Other Frameworks
┌─────────────────────────────────────────────────────────────────────────────┐
│ CONCURRENCY MODEL COMPARISON │
├─────────────────┬───────────────────────────────────────────────────────────┤
│ Framework │ How it handles 10,000 concurrent connections │
├─────────────────┼───────────────────────────────────────────────────────────┤
│ │ │
│ Ruby on Rails │ Thread pool (typically 5-25 threads) │
│ │ Each request blocks a thread │
│ │ Need load balancer + many processes │
│ │ │
│ Django │ Similar to Rails (WSGI is synchronous) │
│ │ ASGI + async views help but add complexity │
│ │ │
│ Node.js/Express │ Single-threaded event loop │
│ │ Non-blocking I/O, callbacks/promises │
│ │ CPU-bound work blocks everything │
│ │ Need worker threads or cluster mode │
│ │ │
│ Go (net/http) │ Goroutines (lightweight, like BEAM processes) │
│ │ Excellent concurrency, but manual error handling │
│ │ No supervision trees │
│ │ │
│ Phoenix/Elixir │ 10,000 BEAM processes (one per connection) │
│ │ Preemptive scheduling, no blocking │
│ │ Supervision trees auto-restart failed processes │
│ │ Built for "let it crash" philosophy │
│ │ │
└─────────────────┴───────────────────────────────────────────────────────────┘
Feature Comparison
| Feature | Phoenix | Rails | Django | Express | Go (std lib) |
|---|---|---|---|---|---|
| Real-time (WebSockets) | Built-in Channels + LiveView | ActionCable (bolt-on) | Channels (bolt-on) | Socket.io (3rd party) | gorilla/websocket |
| Concurrency Model | Actor model (millions of processes) | Thread pool | Thread pool (async optional) | Event loop | Goroutines |
| Fault Tolerance | Supervisor trees (auto-restart) | External (systemd) | External | External | Manual |
| Hot Code Reload | Yes (BEAM feature) | No | No | No | No |
| Database | Ecto (functional) | ActiveRecord (ORM) | Django ORM | Choose your own | database/sql |
| Learning Curve | Steep (new paradigm) | Low | Low | Low | Medium |
| Community Size | Small but growing | Very large | Very large | Massive | Large |
| Performance | Excellent | Good | Good | Excellent | Excellent |
| Productivity | High (once learned) | Very High | Very High | Medium | Medium |
When to Choose Phoenix
Phoenix excels at:
- Real-time features (chat, live updates, notifications)
- High concurrency (many simultaneous connections)
- Fault-tolerant systems (financial, telecom)
- Long-running connections (IoT, streaming)
- Applications that need to scale horizontally
Consider alternatives when:
- Team has no functional programming experience and can’t invest in learning
- Hiring is a concern (smaller developer pool)
- You need extensive third-party library ecosystem
- Simple CRUD apps where Rails/Django productivity wins
Project List
Projects are ordered from foundational understanding to advanced implementations.
Project 1: Build a TCP Echo Server (Understanding BEAM Processes)
- File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
- Main Programming Language: Elixir
- Alternative Programming Languages: Erlang
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Concurrency / Networking
- Software or Tool: Elixir, :gen_tcp
- Main Book: “Elixir in Action” by Saša Jurić
What you’ll build: A TCP server that accepts multiple simultaneous connections, echoing back whatever clients send. Each connection is handled by its own lightweight BEAM process.
Why it teaches Phoenix: Before Phoenix, there’s Elixir. Before Elixir, there’s the BEAM. This project shows you the foundation—how lightweight processes work, how they communicate via messages, and why this model enables Phoenix’s performance.
Core challenges you’ll face:
- Spawning processes for each connection → maps to the actor model
- Message passing between processes → maps to how Phoenix Channels work
- Handling process crashes → maps to fault tolerance
- Using :gen_tcp → maps to understanding Cowboy’s foundation
Key Concepts:
- BEAM Processes: “Elixir in Action” Chapter 5 - Saša Jurić
- Message Passing: “Elixir in Action” Chapter 5
- :gen_tcp module: Erlang/Elixir documentation
- Process Linking: “Elixir in Action” Chapter 8
Difficulty: Intermediate Time estimate: Weekend Prerequisites: Basic Elixir syntax (pattern matching, functions, modules). Install Elixir.
Real world outcome:
# Terminal 1: Start your server
$ iex -S mix
iex> EchoServer.start(4000)
Listening on port 4000...
# Terminal 2: Connect with telnet
$ telnet localhost 4000
Connected to localhost.
Hello, BEAM!
Hello, BEAM! # Server echoes back
# Terminal 3: Another simultaneous connection
$ telnet localhost 4000
Connected to localhost.
Second connection!
Second connection!
# Both connections work independently, each in its own process!
Implementation Hints:
The core pattern for accepting connections:
1. Open a listening socket with :gen_tcp.listen/2
2. Accept a connection with :gen_tcp.accept/1
3. Spawn a new process to handle this connection
4. Go back to step 2 (accept loop)
For each client process:
1. Receive data with :gen_tcp.recv/2
2. Send it back with :gen_tcp.send/2
3. Loop until connection closes
Key questions to answer:
- What happens when you
spawna function? - How does
receiveblock a process without blocking others? - What happens if a client process crashes? Does the server crash?
- How many connections can you handle simultaneously?
Resources for key challenges:
Learning milestones:
- Single connection works → You understand :gen_tcp basics
- Multiple connections work simultaneously → You understand process spawning
- Server survives client crashes → You understand process isolation
- You can track active connections → You understand process communication
Project 2: Add Supervision Trees (Fault Tolerance)
- File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
- Main Programming Language: Elixir
- Alternative Programming Languages: Erlang
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: OTP / Fault Tolerance
- Software or Tool: Elixir, OTP Supervisor
- Main Book: “Elixir in Action” by Saša Jurić
What you’ll build: Wrap your TCP server in a supervision tree that automatically restarts failed components. Crash the acceptor? It restarts. Crash a connection handler? Only that connection dies.
Why it teaches Phoenix: Phoenix applications are OTP applications with supervision trees. Understanding Supervisors is essential for understanding how Phoenix stays resilient under load.
Core challenges you’ll face:
- Designing a supervision tree → maps to Phoenix’s application structure
- Choosing restart strategies → maps to one_for_one vs one_for_all
- GenServer behavior → maps to how Channels and PubSub work
- Application behavior → maps to how Phoenix apps start
Key Concepts:
- Supervisors: “Elixir in Action” Chapter 8 - Saša Jurić
- GenServer: “Elixir in Action” Chapter 6
- Application Behavior: “Elixir in Action” Chapter 9
- Restart Strategies: OTP documentation
Difficulty: Intermediate Time estimate: Weekend to 1 week Prerequisites: Project 1 (TCP server). Understanding of basic OTP concepts.
Real world outcome:
$ iex -S mix
iex> EchoServer.Application.start(:normal, [])
{:ok, #PID<0.150.0>}
# View the supervision tree
iex> :observer.start()
# Opens GUI showing:
# EchoServer.Application
# └── EchoServer.Supervisor
# ├── EchoServer.Acceptor (GenServer)
# └── EchoServer.ConnectionSupervisor (DynamicSupervisor)
# ├── Connection #PID<0.200.0>
# ├── Connection #PID<0.201.0>
# └── Connection #PID<0.202.0>
# Kill the acceptor - it restarts automatically!
iex> Process.exit(pid, :kill)
# Logs: Acceptor crashed, restarting...
# Server continues working!
Implementation Hints:
Supervision tree structure:
Application
│
└── Supervisor (one_for_one)
│
├── Acceptor (GenServer)
│ └── Accepts connections, spawns handlers
│
└── ConnectionSupervisor (DynamicSupervisor)
└── Dynamically supervises connection handlers
GenServer callback skeleton:
defmodule EchoServer.Acceptor do
use GenServer
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(port) do
# Open listening socket
{:ok, listen_socket} = :gen_tcp.listen(port, [...])
# Start accepting (use send_after to avoid blocking init)
send(self(), :accept)
{:ok, %{socket: listen_socket}}
end
@impl true
def handle_info(:accept, state) do
# Accept connection, spawn handler, loop
{:noreply, state}
end
end
Learning milestones:
- Supervisor starts child processes → You understand supervision basics
- Crashed process restarts automatically → You understand restart strategies
- Dynamic supervisor manages connections → You understand DynamicSupervisor
- :observer shows your tree → You can visualize OTP applications
Project 3: Build a Minimal Plug Application
- File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
- Main Programming Language: Elixir
- Alternative Programming Languages: N/A
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Web / HTTP
- Software or Tool: Plug, Cowboy
- Main Book: “Programming Phoenix 1.4” by Chris McCord
What you’ll build: A web application using only Plug and Cowboy—no Phoenix. You’ll see exactly what Phoenix does for you by doing it yourself: routing, parsing, rendering, and middleware.
Why it teaches Phoenix: Phoenix is built on Plug. Every Phoenix endpoint, router, and controller is a Plug. Understanding Plug means understanding Phoenix’s core abstraction.
Core challenges you’ll face:
- The Plug specification → maps to function and module plugs
- The Conn struct → maps to request/response data structure
- Plug pipelines → maps to Phoenix pipelines
- Plug.Router → maps to Phoenix.Router
Key Concepts:
- Plug Specification: Plug Documentation
- Conn Struct: Understanding request/response state
- Cowboy: Erlang HTTP server
- Pipelines: Composing transformations
Difficulty: Intermediate Time estimate: Weekend Prerequisites: Basic Elixir, understanding of HTTP. Projects 1-2 helpful but not required.
Real world outcome:
$ iex -S mix
iex> MiniWeb.Application.start(:normal, [])
Server running at http://localhost:4000
$ curl http://localhost:4000/
Welcome to MiniWeb!
$ curl http://localhost:4000/hello/world
Hello, world!
$ curl http://localhost:4000/users -d '{"name": "Alice"}'
Created user: Alice
$ curl http://localhost:4000/unknown
404 Not Found
Implementation Hints:
Minimal Plug module:
defmodule MiniWeb.Router do
use Plug.Router
plug :match
plug :dispatch
get "/" do
send_resp(conn, 200, "Welcome to MiniWeb!")
end
get "/hello/:name" do
send_resp(conn, 200, "Hello, #{name}!")
end
match _ do
send_resp(conn, 404, "Not Found")
end
end
The Conn struct (simplified):
%Plug.Conn{
host: "localhost",
port: 4000,
method: "GET",
path_info: ["hello", "world"],
params: %{"name" => "world"},
req_headers: [...],
resp_headers: [...],
status: nil, # Set by send_resp
resp_body: nil, # Set by send_resp
assigns: %{}, # Your custom data
...
}
Key insight: A Plug is a function that takes a conn, transforms it, and returns a conn. That’s it!
Resources for key challenges:
Learning milestones:
- Cowboy serves your Plug → You understand the HTTP server layer
- Routes match correctly → You understand Plug.Router
- Custom plugs transform requests → You understand middleware
- You parse JSON bodies → You understand plug pipelines
Project 4: Your First Phoenix Application
- File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
- Main Programming Language: Elixir
- Alternative Programming Languages: N/A
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Web Framework
- Software or Tool: Phoenix Framework
- Main Book: “Programming Phoenix 1.4” by Chris McCord
What you’ll build: A standard Phoenix CRUD application—but with deep understanding of every generated file and concept. You’ll trace a request through the entire stack.
Why it teaches Phoenix: Now that you understand BEAM processes, OTP, and Plug, you can appreciate what Phoenix provides. This project connects the dots between the foundations and the framework.
Core challenges you’ll face:
- Phoenix project structure → maps to where things live and why
- Contexts (Phoenix 1.3+) → maps to domain-driven design
- Ecto basics → maps to database interactions
- Templates (HEEx) → maps to HTML generation
Key Concepts:
- Phoenix Architecture: Phoenix Overview
- Contexts: “Programming Phoenix 1.4” Chapter 2
- Router & Pipelines: “Programming Phoenix 1.4” Chapter 2
- Controllers & Views: “Programming Phoenix 1.4” Chapter 3
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Projects 1-3 (foundations), basic SQL knowledge.
Real world outcome:
$ mix phx.new blog
$ cd blog
$ mix ecto.create
$ mix phx.gen.html Content Post posts title:string body:text
$ mix ecto.migrate
$ mix phx.server
# Browser: http://localhost:4000/posts
# Full CRUD interface for blog posts!
# You understand:
# - Why files are organized this way
# - How the router dispatches to controllers
# - How Ecto persists data
# - How templates render HTML
# - How the whole request flows through the stack
Implementation Hints:
Phoenix project structure:
blog/
├── lib/
│ ├── blog/ # Business logic (contexts)
│ │ ├── content.ex # Content context
│ │ └── content/
│ │ └── post.ex # Post schema
│ ├── blog_web/ # Web interface
│ │ ├── controllers/
│ │ ├── components/ # (Phoenix 1.7+) or templates/
│ │ ├── router.ex
│ │ └── endpoint.ex
│ ├── blog.ex # Application module
│ └── blog_web.ex # Web module macros
├── config/ # Configuration
├── priv/
│ └── repo/
│ └── migrations/ # Database migrations
└── test/ # Tests
The request flow for GET /posts:
1. Endpoint receives HTTP request
2. Router matches "/posts" to PostController.index
3. Pipeline `:browser` applies session, flash, CSRF plugs
4. PostController.index calls Content.list_posts()
5. Context queries database via Ecto
6. Controller renders "index.html" with posts
7. Template generates HTML
8. Response sent to browser
Learning milestones:
- Generated app runs → You understand project structure
- You can trace a request through the stack → You understand the flow
- You modify a context function → You understand the boundary
- You add a new route and controller → You understand the patterns
Project 5: Deep Dive into Ecto
- File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
- Main Programming Language: Elixir
- Alternative Programming Languages: N/A
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 3: Advanced
- Knowledge Area: Database / Functional
- Software or Tool: Ecto, PostgreSQL
- Main Book: “Programming Ecto” by Darin Wilson
What you’ll build: A data-intensive application that uses Ecto’s advanced features: complex queries, associations, transactions, custom types, and understanding why Ecto is NOT an ORM.
Why it teaches Phoenix: Ecto is Elixir’s database toolkit. Unlike ActiveRecord or Django ORM, it’s explicitly functional—no hidden state, explicit changesets, composable queries. Understanding Ecto’s philosophy is essential for Phoenix development.
Core challenges you’ll face:
- Changesets → maps to validating and casting data
- Composable queries → maps to building queries piece by piece
- Associations → maps to has_many, belongs_to, many_to_many
- Transactions → maps to multi-step database operations
Key Concepts:
- Repos: “Programming Ecto” Chapter 2 - All DB operations go through Repo
- Schemas: “Programming Ecto” Chapter 3 - Mapping DB to Elixir
- Changesets: “Programming Ecto” Chapter 4 - Validating changes
- Query: “Programming Ecto” Chapter 5 - Composable queries
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Project 4 (basic Phoenix), SQL knowledge.
Real world outcome:
# Complex composable query
query = from p in Post,
join: u in assoc(p, :user),
where: p.published == true,
where: p.inserted_at > ^one_week_ago,
order_by: [desc: p.inserted_at],
preload: [:user, :comments],
select: %{title: p.title, author: u.name, comment_count: count(p.comments)}
posts = Repo.all(query)
# Changeset with validations
def changeset(user, attrs) do
user
|> cast(attrs, [:email, :password, :name])
|> validate_required([:email, :password])
|> validate_format(:email, ~r/@/)
|> validate_length(:password, min: 8)
|> unique_constraint(:email)
|> put_password_hash()
end
# Transaction for complex operations
Repo.transaction(fn ->
with {:ok, user} <- Accounts.create_user(attrs),
{:ok, profile} <- Profiles.create_profile(user, profile_attrs),
:ok <- Mailer.send_welcome_email(user) do
{:ok, user}
else
{:error, reason} -> Repo.rollback(reason)
end
end)
Implementation Hints:
Ecto is NOT an ORM - key differences:
ActiveRecord (ORM) Ecto (Functional Toolkit)
────────────────── ──────────────────────────
user.save Repo.insert(changeset)
↓ ↓
Object tracks its own Changeset is passed
dirty state explicitly to Repo
user.posts.build(...) build_assoc(user, :posts, ...)
↓ ↓
Implicit association Explicit function call
magic
User.find(1) Repo.get(User, 1)
↓ ↓
Model has class methods Repo handles all queries
user.posts Repo.preload(user, :posts)
↓ ↓
Lazy loading (N+1 trap) Explicit preloading
Resources for key challenges:
Learning milestones:
- You write composable queries → You understand Ecto.Query
- Changeset validates and transforms → You understand the pattern
- Associations load explicitly → You understand preloading
- Transaction handles failures → You understand Multi and rollbacks
Project 6: Phoenix Channels (Real-Time Communication)
- File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
- Main Programming Language: Elixir + JavaScript
- Alternative Programming Languages: N/A
- Coolness Level: Level 4: Hardcore Tech Flex
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Real-Time / WebSockets
- Software or Tool: Phoenix Channels, JavaScript client
- Main Book: “Programming Phoenix 1.4” by Chris McCord
What you’ll build: A real-time chat application where messages appear instantly for all users without page refresh. You’ll understand Phoenix’s pub-sub system and how millions of connections are handled.
Why it teaches Phoenix: Channels showcase Phoenix’s killer feature: real-time at scale. Each WebSocket connection is a lightweight BEAM process. Phoenix PubSub distributes messages. This is why Discord uses Elixir.
Core challenges you’ll face:
- Socket lifecycle → maps to connect, join, handle_in, terminate
- Topics and rooms → maps to pub-sub patterns
- Presence → maps to tracking who’s online
- JavaScript client → maps to browser-side integration
Key Concepts:
- Channels: “Programming Phoenix 1.4” Chapter 11
- PubSub: Phoenix.PubSub documentation
- Presence: Phoenix.Presence documentation
- JavaScript Client: phoenix.js documentation
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Project 4 (Phoenix basics), JavaScript knowledge.
Real world outcome:
┌─────────────────────────────────────────────────────────────────┐
│ Phoenix Chat - Room: #general │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Alice: Hey everyone! 10:30 AM │
│ Bob: Hi Alice! How's it going? 10:31 AM │
│ Charlie: Great to see you both! 10:31 AM │
│ │
│ ───────────────────────────────────────────────────────────── │
│ Online: Alice, Bob, Charlie (3 users) │
│ ───────────────────────────────────────────────────────────── │
│ │
│ [Type a message...] [Send] │
└─────────────────────────────────────────────────────────────────┘
# Messages appear INSTANTLY for all connected users
# User list updates in real-time as people join/leave
# Each user is a separate BEAM process on the server
Implementation Hints:
Channel lifecycle:
Browser Phoenix
│ │
│── WebSocket connect ─────────────────────────►│
│ UserSocket.connect/3
│◄───────────────────── :ok ────────────────────│
│ │
│── channel.join("room:lobby") ────────────────►│
│ RoomChannel.join/3
│◄───────────────────── :ok ────────────────────│
│ │
│── channel.push("new_msg", {body: "Hi"}) ─────►│
│ RoomChannel.handle_in/3
│ broadcast!(socket, "new_msg", ...)
│◄─────────── broadcast to all in room ─────────│
│ │
Channel module structure:
defmodule MyAppWeb.RoomChannel do
use MyAppWeb, :channel
def join("room:" <> room_id, _params, socket) do
# Called when client joins this topic
{:ok, assign(socket, :room_id, room_id)}
end
def handle_in("new_msg", %{"body" => body}, socket) do
# Handle incoming message from this client
broadcast!(socket, "new_msg", %{body: body, user: socket.assigns.user})
{:noreply, socket}
end
end
Resources for key challenges:
Learning milestones:
- WebSocket connects → You understand Socket
- Messages broadcast to all → You understand pub-sub
- Presence tracks online users → You understand Presence
- Multiple rooms work → You understand topics
Project 7: Phoenix LiveView (Interactive UI without JavaScript)
- File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
- Main Programming Language: Elixir (with minimal JavaScript)
- Alternative Programming Languages: N/A
- Coolness Level: Level 5: Pure Magic
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 3: Advanced
- Knowledge Area: Real-Time / UI
- Software or Tool: Phoenix LiveView
- Main Book: “Programming Phoenix LiveView” by Bruce Tate
What you’ll build: A fully interactive single-page application with search-as-you-type, form validation, sorting, pagination, and real-time updates—all without writing JavaScript. LiveView sends minimal DOM diffs over WebSocket.
Why it teaches Phoenix: LiveView is Phoenix’s most innovative feature. It combines the productivity of server-rendered apps with the interactivity of SPAs. Understanding how it tracks state and computes diffs teaches you the framework’s core philosophy.
Core challenges you’ll face:
- LiveView lifecycle → maps to mount, handle_event, render
- Socket assigns → maps to state management
- DOM patching → maps to how minimal updates work
- Live navigation → maps to SPA-like routing
Key Concepts:
- LiveView Lifecycle: Phoenix LiveView Introduction
- Assigns & Diff Tracking: How Phoenix LiveView Works
- Events & Bindings: LiveView documentation
- Live Components: Reusable LiveView pieces
Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Projects 4-6 (Phoenix and Channels). Understanding of HTML/CSS.
Real world outcome:
┌─────────────────────────────────────────────────────────────────┐
│ Product Catalog [🔍 Search...] │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Sort by: [Name ▼] [Price ▼] Filter: [All Categories] │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ 📱 │ │ 💻 │ │ 🎧 │ │ ⌚ │ │
│ │ iPhone │ │ MacBook │ │ AirPods │ │ Watch │ │
│ │ $999 │ │ $1,299 │ │ $249 │ │ $399 │ │
│ │ [Add 🛒] │ │ [Add 🛒] │ │ [Add 🛒] │ │ [Add 🛒] │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ Showing 1-4 of 24 products [◀ Prev] [1] [2] [3] [Next ▶] │
│ │
│ Cart: 3 items ($1,647) [Checkout] │
└─────────────────────────────────────────────────────────────────┘
# As you type in search: results filter instantly
# Click column header: sorts without page reload
# Add to cart: cart updates, counter changes
# ALL without writing JavaScript—just Elixir!
Implementation Hints:
LiveView module structure:
defmodule MyAppWeb.ProductLive.Index do
use MyAppWeb, :live_view
def mount(_params, _session, socket) do
# Called on initial load AND WebSocket connect
products = Products.list_products()
{:ok, assign(socket, products: products, cart: [])}
end
def handle_event("search", %{"query" => query}, socket) do
# Called when user types in search box
products = Products.search(query)
{:noreply, assign(socket, products: products)}
end
def handle_event("add_to_cart", %{"id" => id}, socket) do
product = Products.get_product!(id)
cart = [product | socket.assigns.cart]
{:noreply, assign(socket, cart: cart)}
end
def render(assigns) do
~H"""
<input type="text" phx-keyup="search" placeholder="Search..." />
<div class="products">
<%= for product <- @products do %>
<div class="product">
<h3><%= product.name %></h3>
<button phx-click="add_to_cart" phx-value-id={product.id}>
Add to Cart
</button>
</div>
<% end %>
</div>
<div class="cart">Items: <%= length(@cart) %></div>
"""
end
end
How diff tracking works:
Template divides into static and dynamic parts:
~H"""
<div class="product"> ← static (sent once)
<h3><%= @product.name %></h3> ← dynamic (tracked)
<p>$<%= @product.price %></p> ← dynamic (tracked)
</div> ← static (sent once)
"""
When @product.price changes from 99 to 89:
- Server ONLY sends: {position_2: "89"}
- Client patches just that text node
- No full HTML re-render!
Resources for key challenges:
Learning milestones:
- Mount and render work → You understand the lifecycle
- Events update state → You understand handle_event
- You see minimal diff in DevTools → You understand optimization
- Live navigation works → You understand SPA-like behavior
Project 8: Authentication from Scratch
- File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
- Main Programming Language: Elixir
- Alternative Programming Languages: N/A
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 3: Advanced
- Knowledge Area: Security / Web
- Software or Tool: Phoenix, Argon2, JWT (optional)
- Main Book: “Programming Phoenix 1.4” by Chris McCord
What you’ll build: A complete authentication system with registration, login, logout, password hashing, session management, and protected routes. You’ll understand both how phx.gen.auth works and build the key pieces manually.
Why it teaches Phoenix: Authentication touches many Phoenix concepts: plugs (for auth checks), contexts (for user management), Ecto (for password hashing), sessions, and LiveView integration. It’s a great integration project.
Core challenges you’ll face:
- Password hashing → maps to bcrypt/argon2, never store plaintext
- Session management → maps to cookies, tokens, security
- Auth plugs → maps to protecting routes
- LiveView auth → maps to socket assigns, on_mount
Key Concepts:
- Password Hashing: Comeonin/Argon2 libraries
- Plug.Session: Cookie-based sessions
- CSRF Protection: Phoenix built-in
- phx.gen.auth: Phoenix auth generator
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Projects 4-5 (Phoenix, Ecto), understanding of web security basics.
Real world outcome:
# Registration
POST /users/register
{email: "alice@example.com", password: "secret123"}
→ Creates user with hashed password
→ Sets session token
→ Redirects to dashboard
# Login
POST /users/login
{email: "alice@example.com", password: "secret123"}
→ Verifies password hash
→ Sets session token
→ Redirects to dashboard
# Protected route
GET /dashboard
→ Auth plug checks session
→ If valid: shows dashboard with current_user
→ If invalid: redirects to login
# LiveView protected page
→ on_mount hook assigns current_user to socket
→ If no user: redirect to login
Implementation Hints:
Auth plug for protecting routes:
defmodule MyAppWeb.Plugs.RequireAuth do
import Plug.Conn
import Phoenix.Controller
def init(opts), do: opts
def call(conn, _opts) do
if conn.assigns[:current_user] do
conn
else
conn
|> put_flash(:error, "You must log in to access this page")
|> redirect(to: "/login")
|> halt()
end
end
end
# In router:
pipeline :protected do
plug :fetch_current_user
plug MyAppWeb.Plugs.RequireAuth
end
Password hashing with Argon2:
# In changeset
def registration_changeset(user, attrs) do
user
|> cast(attrs, [:email, :password])
|> validate_required([:email, :password])
|> validate_length(:password, min: 8)
|> hash_password()
end
defp hash_password(changeset) do
case get_change(changeset, :password) do
nil -> changeset
password ->
put_change(changeset, :password_hash, Argon2.hash_pwd_salt(password))
end
end
Learning milestones:
- Passwords are hashed correctly → You understand security basics
- Sessions persist across requests → You understand session management
- Protected routes redirect → You understand auth plugs
- LiveView pages are protected → You understand on_mount hooks
Project 9: Build a REST API with JSON:API or GraphQL
- File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
- Main Programming Language: Elixir
- Alternative Programming Languages: N/A
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: API Design
- Software or Tool: Phoenix, Absinthe (for GraphQL), or JSON:API
- Main Book: “Craft GraphQL APIs in Elixir with Absinthe” by Bruce Williams
What you’ll build: A well-designed API (REST or GraphQL) with authentication, pagination, filtering, proper error handling, and documentation. You’ll understand Phoenix’s API capabilities.
Why it teaches Phoenix: Phoenix isn’t just for HTML. Its lightweight nature and Elixir’s pattern matching make it excellent for APIs. Understanding the :api pipeline versus :browser teaches important architectural decisions.
Core challenges you’ll face:
- API pipeline → maps to JSON rendering, no session
- Error handling → maps to fallback controllers
- GraphQL types → maps to Absinthe schema
- Pagination → maps to cursor vs offset pagination
Key Concepts:
- Phoenix JSON API: Phoenix documentation
- Absinthe GraphQL: “Craft GraphQL APIs in Elixir with Absinthe”
- API Authentication: Token-based auth
- Error Handling: FallbackController pattern
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Projects 4-5 (Phoenix, Ecto), understanding of REST/GraphQL concepts.
Real world outcome:
# REST API
$ curl -H "Authorization: Bearer <token>" \
http://localhost:4000/api/posts
{
"data": [
{"id": 1, "title": "Hello Phoenix", "author": "Alice"},
{"id": 2, "title": "Elixir Rocks", "author": "Bob"}
],
"meta": {"total": 42, "page": 1, "per_page": 10}
}
# GraphQL API
$ curl -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ posts { title author { name } } }"}'
{
"data": {
"posts": [
{"title": "Hello Phoenix", "author": {"name": "Alice"}},
{"title": "Elixir Rocks", "author": {"name": "Bob"}}
]
}
}
Implementation Hints:
API pipeline (no session, HTML rendering):
pipeline :api do
plug :accepts, ["json"]
plug MyAppWeb.Plugs.APIAuth
end
scope "/api", MyAppWeb.API do
pipe_through :api
resources "/posts", PostController, except: [:new, :edit]
end
FallbackController for error handling:
defmodule MyAppWeb.FallbackController do
use MyAppWeb, :controller
def call(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> json(%{error: "Not found"})
end
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
conn
|> put_status(:unprocessable_entity)
|> json(%{errors: format_errors(changeset)})
end
end
Learning milestones:
- API returns JSON → You understand the api pipeline
- Auth protects endpoints → You understand token auth
- Errors return proper status codes → You understand FallbackController
- GraphQL queries work → You understand Absinthe
Project 10: PubSub and Distributed Phoenix
- File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
- Main Programming Language: Elixir
- Alternative Programming Languages: N/A
- Coolness Level: Level 5: Pure Magic
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 4: Expert
- Knowledge Area: Distributed Systems
- Software or Tool: Phoenix.PubSub, libcluster
- Main Book: “Elixir in Action” by Saša Jurić
What you’ll build: A Phoenix application that runs on multiple nodes, with PubSub messages reaching all connected users across all nodes. You’ll see how Phoenix scales horizontally.
Why it teaches Phoenix: Phoenix’s ability to scale across multiple nodes—with Channels, LiveView, and PubSub working seamlessly—is why companies choose it for high-traffic applications. Understanding distribution is understanding Phoenix’s power.
Core challenges you’ll face:
- Connecting BEAM nodes → maps to libcluster, Erlang distribution
- PubSub across nodes → maps to :pg2, Phoenix.PubSub
- Session sharing → maps to Redis, distributed cache
- Deployment → maps to releases, clustering in production
Key Concepts:
- Erlang Distribution: Node connections, cookies
- PubSub Adapters: PG2 (built-in), Redis
- libcluster: Auto-discovery of nodes
- Releases:
mix release
Difficulty: Expert Time estimate: 2-3 weeks Prerequisites: Projects 6-7 (Channels, LiveView), understanding of distributed systems.
Real world outcome:
# Start node 1
$ PORT=4000 iex --name node1@127.0.0.1 -S mix phx.server
# Start node 2
$ PORT=4001 iex --name node2@127.0.0.1 -S mix phx.server
# Nodes discover each other (libcluster)
[libcluster] Connected to node2@127.0.0.1
# User A connects to node1, User B connects to node2
# User A sends message in chat
# User B receives it INSTANTLY (PubSub across nodes)
# In iex on node1:
iex(node1@127.0.0.1)> Node.list()
[:node2@127.0.0.1]
iex(node1@127.0.0.1)> Phoenix.PubSub.broadcast(MyApp.PubSub, "room:lobby", {:msg, "Hi!"})
# Message received on BOTH nodes!
Implementation Hints:
libcluster configuration:
# config/runtime.exs
config :libcluster,
topologies: [
local: [
strategy: Cluster.Strategy.Gossip
]
]
PubSub across nodes:
Node 1 Node 2
┌─────────────────────┐ ┌─────────────────────┐
│ Phoenix.PubSub │ │ Phoenix.PubSub │
│ (local subscribers) │◄────────────►│ (local subscribers) │
│ │ pg2/Redis │ │
│ User A (WebSocket) │ │ User B (WebSocket) │
└─────────────────────┘ └─────────────────────┘
When User A sends message:
1. Channel broadcasts to local PubSub
2. PubSub adapter forwards to other nodes
3. Other nodes broadcast to their local subscribers
4. User B receives message
Learning milestones:
- Two nodes connect → You understand Erlang distribution
- PubSub reaches both nodes → You understand distributed PubSub
- LiveView works across nodes → You understand horizontal scaling
- You can add/remove nodes dynamically → You understand clustering
Project 11: Background Jobs with Oban
- File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
- Main Programming Language: Elixir
- Alternative Programming Languages: N/A
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Background Processing
- Software or Tool: Oban
- Main Book: Oban documentation
What you’ll build: A robust background job system for sending emails, processing images, syncing data, and other tasks that shouldn’t block web requests. You’ll understand job queues, retries, and scheduling.
Why it teaches Phoenix: Real applications need background processing. Oban is the standard for Elixir, leveraging PostgreSQL for reliability. Understanding async patterns complements your synchronous Phoenix knowledge.
Core challenges you’ll face:
- Job definition → maps to workers and args
- Queues and concurrency → maps to controlling parallelism
- Retries and backoff → maps to handling failures
- Scheduling → maps to cron-like recurring jobs
Key Concepts:
- Oban Workers: Defining job behavior
- Queues: Controlling concurrency
- Pruning: Cleaning old jobs
- Telemetry: Monitoring job execution
Difficulty: Advanced Time estimate: 1 week Prerequisites: Project 4-5 (Phoenix, Ecto), PostgreSQL.
Real world outcome:
# Enqueue a job
%{user_id: 123, email: "welcome"}
|> MyApp.Workers.SendEmail.new()
|> Oban.insert()
# Job executes in background
# defmodule MyApp.Workers.SendEmail do
# use Oban.Worker, queue: :emails, max_attempts: 3
#
# def perform(%Job{args: %{"user_id" => user_id, "email" => email}}) do
# user = Accounts.get_user!(user_id)
# Mailer.send(user, email)
# end
# end
# Web UI shows:
# ┌────────────────────────────────────────────────────────────┐
# │ Oban Dashboard │
# ├────────────────────────────────────────────────────────────┤
# │ Queues: │
# │ emails: 5 available, 2 executing, 0 failed │
# │ images: 10 available, 5 executing, 1 retrying │
# │ sync: 0 available, 0 executing, 0 failed │
# │ │
# │ Recent Jobs: │
# │ SendEmail (user:123) - completed 5s ago │
# │ ProcessImage (file:abc) - executing... │
# │ SendEmail (user:124) - completed 10s ago │
# └────────────────────────────────────────────────────────────┘
Implementation Hints:
Oban worker with retry:
defmodule MyApp.Workers.ProcessImage do
use Oban.Worker,
queue: :images,
max_attempts: 5,
priority: 1
@impl Oban.Worker
def perform(%Job{args: %{"image_id" => id}}) do
image = Media.get_image!(id)
case ImageProcessor.resize(image) do
{:ok, resized} ->
Media.update_image(image, %{processed: true, url: resized.url})
:ok
{:error, reason} ->
# Returning error triggers retry with backoff
{:error, reason}
end
end
end
Learning milestones:
- Jobs execute in background → You understand async processing
- Failed jobs retry → You understand error handling
- Queues control concurrency → You understand resource management
- Scheduled jobs run on time → You understand cron-like behavior
Project 12: Telemetry and Observability
- File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
- Main Programming Language: Elixir
- Alternative Programming Languages: N/A
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Monitoring / Observability
- Software or Tool: Telemetry, PromEx, LiveDashboard
- Main Book: “Elixir in Action” by Saša Jurić
What you’ll build: Full observability for your Phoenix app: request timing, database query metrics, custom business metrics, Prometheus integration, and live dashboards.
Why it teaches Phoenix: Phoenix is built on Telemetry—it emits events for everything. Understanding how to capture and act on these events is essential for running Phoenix in production.
Core challenges you’ll face:
- Telemetry events → maps to what Phoenix emits
- Handlers and metrics → maps to collecting data
- LiveDashboard → maps to built-in monitoring
- Prometheus/Grafana → maps to production monitoring
Key Concepts:
- :telemetry library: Event emission and handling
- Phoenix.Telemetry: Built-in Phoenix events
- PromEx: Prometheus metrics for Phoenix
- LiveDashboard: Built-in Phoenix dashboard
Difficulty: Advanced Time estimate: 1 week Prerequisites: Project 4 (Phoenix basics), understanding of metrics concepts.
Real world outcome:
LiveDashboard (http://localhost:4000/dashboard)
┌─────────────────────────────────────────────────────────────────┐
│ Phoenix LiveDashboard │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Home │ Metrics │ Request Logger │ Applications │ Processes │
│ │
│ System: │
│ BEAM Memory: 234 MB │ Atoms: 12,453 │ Processes: 1,234 │
│ CPU: 15% │ Run Queue: 0 │
│ │
│ Phoenix: │
│ Requests/sec: 1,234 │ Avg Latency: 12ms │ 99p: 45ms │
│ WebSocket connections: 567 │
│ │
│ Ecto: │
│ Queries/sec: 2,345 │ Avg Time: 2ms │ Pool Size: 10 │
│ │
│ Custom Metrics: │
│ Signups today: 123 │ Orders: 45 │ Revenue: $12,345 │
└─────────────────────────────────────────────────────────────────┘
Implementation Hints:
Attaching to Phoenix telemetry events:
# In application.ex
def start(_type, _args) do
:telemetry.attach_many(
"my-app-handler",
[
[:phoenix, :endpoint, :stop],
[:my_app, :repo, :query],
[:my_app, :user, :signup]
],
&MyApp.Telemetry.handle_event/4,
nil
)
# ... start supervision tree
end
# Handler
defmodule MyApp.Telemetry do
def handle_event([:phoenix, :endpoint, :stop], measurements, metadata, _config) do
Logger.info("Request to #{metadata.route} took #{measurements.duration / 1_000_000}ms")
end
end
Learning milestones:
- LiveDashboard shows metrics → You understand built-in observability
- Custom events emit → You understand Telemetry API
- Prometheus scrapes metrics → You understand external monitoring
- Grafana dashboard works → You have production observability
Project 13: Testing Phoenix Applications
- File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
- Main Programming Language: Elixir
- Alternative Programming Languages: N/A
- Coolness Level: Level 2: Practical but Forgettable
- Business Potential: 2. The “Micro-SaaS / Pro Tool”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Testing / Quality
- Software or Tool: ExUnit, Mox, Wallaby
- Main Book: “Testing Elixir” by Andrea Leopardi
What you’ll build: A comprehensive test suite covering unit tests, integration tests, controller tests, LiveView tests, and end-to-end browser tests. You’ll understand Phoenix’s testing patterns.
Why it teaches Phoenix: Phoenix has excellent testing support built-in. Understanding how to test each layer—contexts, controllers, channels, LiveView—makes you a more effective Phoenix developer.
Core challenges you’ll face:
- ConnTest → maps to testing controllers
- DataCase → maps to testing with database
- ChannelCase → maps to testing channels
- LiveViewTest → maps to testing LiveView
Key Concepts:
- ExUnit: Elixir’s testing framework
- ConnTest: Phoenix controller testing
- Mox: Behavior-based mocking
- Sandbox: Database isolation
Difficulty: Intermediate Time estimate: 1 week Prerequisites: Projects 4-7 (Phoenix, Ecto, Channels, LiveView).
Real world outcome:
$ mix test
...............................................................
Finished in 2.3 seconds
42 tests, 0 failures
Randomized with seed 12345
# Test breakdown:
# - 15 context tests (pure business logic)
# - 10 controller tests (HTTP layer)
# - 8 LiveView tests (interactive UI)
# - 5 channel tests (real-time)
# - 4 integration tests (full stack)
Implementation Hints:
Testing a controller:
defmodule MyAppWeb.PostControllerTest do
use MyAppWeb.ConnCase
describe "index" do
test "lists all posts", %{conn: conn} do
post = insert(:post, title: "Hello")
conn = get(conn, ~p"/posts")
assert html_response(conn, 200) =~ "Hello"
end
end
end
Testing LiveView:
defmodule MyAppWeb.CounterLiveTest do
use MyAppWeb.ConnCase
import Phoenix.LiveViewTest
test "increments counter", %{conn: conn} do
{:ok, view, _html} = live(conn, "/counter")
assert view |> element("span.count") |> render() =~ "0"
view |> element("button", "Increment") |> render_click()
assert view |> element("span.count") |> render() =~ "1"
end
end
Learning milestones:
- Context tests pass → You test business logic
- Controller tests pass → You test HTTP layer
- LiveView tests pass → You test interactive UIs
- Full test suite is fast → You understand async testing
Project 14: Deployment and Production Phoenix
- File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
- Main Programming Language: Elixir
- Alternative Programming Languages: Docker, Bash
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: DevOps / Deployment
- Software or Tool: Mix releases, Docker, Fly.io or self-hosted
- Main Book: “Real-World Elixir Deployment” (various resources)
What you’ll build: A production-ready Phoenix deployment with releases, environment configuration, migrations, health checks, and clustering across multiple instances.
Why it teaches Phoenix: Development is only half the story. Understanding how to package Phoenix as a release, handle secrets, run migrations, and scale horizontally completes your knowledge.
Core challenges you’ll face:
- Releases → maps to packaging for production
- Runtime configuration → maps to config/runtime.exs
- Migrations in production → maps to Ecto.Migrator
- Clustering → maps to connecting nodes
Key Concepts:
- Mix Releases:
mix release - Runtime Config: config/runtime.exs
- Docker: Containerized deployment
- Health Checks: Ready/alive probes
Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: All previous projects, Docker knowledge helpful.
Real world outcome:
# Dockerfile
FROM hexpm/elixir:1.15.0-erlang-26.0-alpine-3.18.0 AS build
# ... build release
FROM alpine:3.18.0
COPY --from=build /app/_build/prod/rel/my_app ./
CMD ["bin/my_app", "start"]
# Build and deploy
$ docker build -t my_app .
$ docker push my_registry/my_app
# On Fly.io
$ fly deploy
# Check clustering
$ fly ssh console
> Node.list()
[:my_app@fdaa:0:1234::3] # Connected to peer!
# Run migrations
$ fly ssh console
> MyApp.Release.migrate()
Implementation Hints:
Release configuration:
# config/runtime.exs
import Config
if config_env() == :prod do
config :my_app, MyApp.Repo,
url: System.get_env("DATABASE_URL"),
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
config :my_app, MyAppWeb.Endpoint,
url: [host: System.get_env("PHX_HOST")],
secret_key_base: System.get_env("SECRET_KEY_BASE")
end
Release module for migrations:
defmodule MyApp.Release do
def migrate do
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
end
end
defp repos, do: Application.fetch_env!(:my_app, :ecto_repos)
end
Learning milestones:
- Release builds successfully → You understand mix release
- Docker container runs → You understand containerization
- App starts in production → You understand runtime config
- Multiple instances cluster → You understand horizontal scaling
Project Comparison Table
| Project | Difficulty | Time | Depth of Understanding | Fun Factor |
|---|---|---|---|---|
| 1. TCP Echo Server | Intermediate | Weekend | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 2. Supervision Trees | Intermediate | Weekend-1wk | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 3. Minimal Plug App | Intermediate | Weekend | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 4. First Phoenix App | Intermediate | 1 week | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 5. Ecto Deep Dive | Advanced | 1-2 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 6. Phoenix Channels | Advanced | 1-2 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 7. Phoenix LiveView | Advanced | 2-3 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 8. Authentication | Advanced | 1-2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 9. REST/GraphQL API | Advanced | 1-2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 10. Distributed Phoenix | Expert | 2-3 weeks | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 11. Background Jobs | Advanced | 1 week | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 12. Telemetry | Advanced | 1 week | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| 13. Testing | Intermediate | 1 week | ⭐⭐⭐⭐ | ⭐⭐ |
| 14. Deployment | Advanced | 1-2 weeks | ⭐⭐⭐⭐ | ⭐⭐⭐ |
Recommended Learning Path
If you’re new to Elixir and Phoenix:
- Learn Elixir basics (pattern matching, modules, recursion)
- Project 1: TCP Echo Server → Understand BEAM processes
- Project 2: Supervision Trees → Understand OTP
- Project 3: Minimal Plug → Understand the web layer
- Project 4: First Phoenix App → Connect the dots
- Projects 5-7 → Deep dive into Ecto, Channels, LiveView
- Continue with remaining projects
If you know another web framework (Rails, Django, Express):
- Quick Elixir syntax review
- Project 1-2 (Quick: understand the concurrency model)
- Project 4: First Phoenix App → See Phoenix conventions
- Project 7: LiveView → See what’s unique
- Project 10: Distributed Phoenix → See the scalability story
- Fill in gaps as needed
If you want to understand Phoenix internals:
- Projects 1-3 (essential: understand the layers)
- Read Phoenix source code (it’s very readable!)
- Project 6-7 (Channels, LiveView internals)
- Project 10 (Distribution)
- Consider “Build Your Own Web Framework in Elixir” book
Final Capstone Project: Real-Time Collaborative Application
- File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
- Main Programming Language: Elixir
- Alternative Programming Languages: N/A
- Coolness Level: Level 5: Pure Magic
- Business Potential: 4. The “Open Core” Infrastructure
- Difficulty: Level 5: Master
- Knowledge Area: Full-Stack Phoenix
- Software or Tool: All Phoenix features
- Main Book: All referenced books
What you’ll build: A real-time collaborative document editor (like a mini Google Docs) where multiple users can edit the same document simultaneously, see each other’s cursors, and changes merge correctly.
This project integrates:
- BEAM Processes: Each document is a GenServer
- OTP: Supervision for document processes
- Plug: Request pipeline
- Phoenix: Web framework structure
- Ecto: Document persistence
- Channels: Real-time sync
- LiveView: Interactive UI
- PubSub: Multi-node support
- Presence: Who’s editing what
- Telemetry: Monitoring
- Deployment: Production clustering
Why this is the ultimate Phoenix project: It demonstrates everything Phoenix excels at—real-time, stateful, distributed, fault-tolerant. Companies like Figma, Notion, and Google Docs face these challenges. Building even a simple version proves mastery.
Real world outcome:
┌─────────────────────────────────────────────────────────────────┐
│ Collaborative Editor - Document: "Team Notes" │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Online: 🟢 Alice (editing) 🟢 Bob (viewing) 🟢 Charlie │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ # Meeting Notes │ │
│ │ │ │
│ │ ## Decisions │ │
│ │ - Use Phoenix for the backend ← Alice's cursor │ │
│ │ - Deploy on Fly.io| │ │
│ │ ↑ Bob is typing here │ │
│ │ ## Action Items │ │
│ │ - [ ] Set up CI/CD │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 💾 Auto-saved 2 seconds ago │
└─────────────────────────────────────────────────────────────────┘
Features:
- Real-time sync across all connected users
- Cursor positions shown for other users
- Conflict resolution for concurrent edits
- Works across multiple server nodes
- Survives server restarts (document state recovered)
- Presence shows who's online
Summary
| # | Project | Main Language |
|---|---|---|
| 1 | TCP Echo Server (BEAM Processes) | Elixir |
| 2 | Supervision Trees (Fault Tolerance) | Elixir |
| 3 | Minimal Plug Application | Elixir |
| 4 | First Phoenix Application | Elixir |
| 5 | Deep Dive into Ecto | Elixir |
| 6 | Phoenix Channels (Real-Time) | Elixir + JavaScript |
| 7 | Phoenix LiveView (Interactive UI) | Elixir |
| 8 | Authentication from Scratch | Elixir |
| 9 | REST API with JSON:API or GraphQL | Elixir |
| 10 | PubSub and Distributed Phoenix | Elixir |
| 11 | Background Jobs with Oban | Elixir |
| 12 | Telemetry and Observability | Elixir |
| 13 | Testing Phoenix Applications | Elixir |
| 14 | Deployment and Production | Elixir + Docker |
| Final | Real-Time Collaborative Editor (Capstone) | Elixir |
Key Resources Referenced
Books
- “Elixir in Action” by Saša Jurić (2nd Edition)
- “Programming Phoenix 1.4” by Chris McCord, Bruce Tate, José Valim
- “Programming Phoenix LiveView” by Bruce Tate and Sophie DeBenedetto
- “Programming Ecto” by Darin Wilson and Eric Meadows-Jönsson
- “Craft GraphQL APIs in Elixir with Absinthe” by Bruce Williams
- “Build Your Own Web Framework in Elixir” by Aditya Iyengar