Two Centuries of Elixir
- Published
- Jun 01, 2026
- Reading time
- 1 min
I've been learning Elixir and have already completed my original objectives of being paid to write Elixir professionally. Still, I don't feel I'm remotely close to the level of competency I want to be at. I want to be far, far more adroit with it than I am.
Complexity enters a software project at a few points of ingress. One is the complexity inherent in the problem. Creating an application that intelligently routes network traffic is inherently complex. The other major point of ingress is the complexity introduced by the framework or underlying technology. I want to understand Elixir well enough that the framework complexity is driven to zero.
To do this, I'm embarking on a project that I call "Two Centuries of Elixir". It is 200 projects of varying sizes that target the areas of Elixir I want to reinforce my knowledge of or learn more about. Each project is stand-alone and varies in difficulty from pretty easy to quite hard. Since almost all of the solutions I deliver are web applications, about half of this list focuses on Phoenix.
The Projects
Here are the projects - all 200 of them. They are roughly arranged as a progression, but I'm not putting any constraints on the order in which I finish them. I can pick whatever projects pique my interest.
| Name | Description | Effort |
|---|---|---|
| PART I | CORE ELIXIR & THE BEAM | |
| Phase 1 | Functional Foundations | |
| 1. Roll your own Enum |
Reimplement map, reduce, and filter from scratch with tail recursion, then benchmark them against the built-in Enum.
|
low |
| 2. Lazy infinite sequences |
Build an endless primes or Fibonacci generator with Stream.unfold/2 and pull finite slices with Enum.take/2.
|
low |
3. with pipelines
|
Chain several functions that each return {:ok, _}/{:error, _} using with, so the flow short-circuits on the first error.
|
low |
| 4. Comprehension depth |
Use a for comprehension with multiple generators, guard filters, and :into to build a map or MapSet in one pass.
|
low |
| 5. Lazy file pipeline |
Stream a multi-GB file through File.stream! and Stream transforms so it's processed line-by-line and never fully loaded.
|
low |
| 6. Protocols |
Define a Describable protocol, implement it for a few different structs, and add an Any fallback for everything else.
|
med |
| 7. Custom Enumerable |
Implement the Enumerable protocol for a ring-buffer struct so Enum and Stream functions work on it.
|
med |
| 8. Custom Collectable |
Implement the Collectable protocol for a struct so Enum.into/2 can build it from any enumerable.
|
med |
| 9. Custom Access |
Implement the Access behaviour for a struct so get_in, put_in, and update_in can traverse it.
|
med |
| 10. Behaviours |
Define a behaviour with @callbacks and write two swappable adapter modules that implement it (e.g., two storage backends).
|
low |
| 11. Persistent structure | Implement an immutable zipper or finger tree that supports efficient local updates without copying the entire structure. | high |
| Phase 2 | OTP & Process Design | |
| 12. GenServer fundamentals |
Build a bank-account GenServer with synchronous call and async cast, holding balance in state and handling code_change/3.
|
low |
| 13. Agent vs GenServer |
Implement the same counter once with Agent and once with GenServer, then compare ergonomics and when each fits.
|
low |
| 14. Supervisor strategy lab |
Start several children under a Supervisor and crash them deliberately to observe :one_for_one, :rest_for_one, and :one_for_all.
|
low |
| 15. Babysitter monitor |
Write a process that Process.monitors a worker and logs or reacts to the :DOWN message and its exit reason.
|
med |
16. :gen_statem
|
Model a turnstile or traffic light as an explicit :gen_statem with named states and event-driven transitions.
|
med |
| 17. Process-per-entity |
Spawn one process per chat room/session on demand with DynamicSupervisor and look it up by key via Registry.
|
med |
| 18. Worker factory |
Use a DynamicSupervisor to start and stop worker processes on demand, with graceful shutdown via terminate/2.
|
med |
| 19. NimblePool resource pool |
Pool a scarce resource (e.g., a port or connection) with NimblePool, checking it out and back in per request.
|
med |
| 20. Supervised async tasks |
Run many jobs concurrently with Task.Supervisor.async_stream_nolink/3 capped by :max_concurrency, handling timeouts.
|
med |
| 21. Job queue from scratch |
Build a GenServer dispatcher that hands queued jobs to a fixed pool of worker processes, with no external library.
|
med |
| 22. Circuit breaker |
Wrap a flaky downstream call in a GenServer that fails fast after N errors and half-opens to retry later.
|
med |
| 23. Debounce GenServer |
Build a GenServer that absorbs a burst of events and emits one coalesced result after a quiet period, using Process.send_after.
|
med |
| 24. GenStage backpressure |
Build a GenStage producer/consumer pair where the consumer pulls on demand so the producer never overruns it.
|
med |
| 25. Flow word-count |
Count word frequencies across a large file in parallel with Flow, partitioning the work across all CPU cores.
|
med |
| Phase 3 | The BEAM Up Close | |
| 26. ETS table types |
Create :set, :bag, and :ordered_set ETS tables, compare lookups, and write a :ets.fun2ms match spec to filter rows.
|
med |
| 27. ETS leaderboard |
Build a top-scores board in an :ordered_set ETS table that many processes read concurrently while one process serializes writes.
|
med |
28. :counters / :atomics
|
Share a single integer counter across many processes with :counters and confirm it stays correct without locks.
|
med |
29. :persistent_term vs ETS
|
Store read-mostly config in both :persistent_term and ETS and benchmark read latency to see the trade-off.
|
med |
| 30. Memoizer |
Cache results of an expensive recursive function (e.g., naive Fibonacci) in ETS or a GenServer so repeats are instant.
|
med |
| 31. TTL + LRU cache |
Build an in-memory cache GenServer that expires entries after a TTL and evicts least-recently-used when full; measure hit rate.
|
med |
| 32. Binary internals |
Create refc vs heap binaries, trigger the sub-binary "leak" by slicing a large binary, and watch memory with :erlang.memory.
|
high |
| 33. Mailbox footgun |
Build a process whose selective receive slows as its mailbox grows, then fix it; measure both with :erlang.statistics.
|
high |
| 34. Live tracing |
Attach to a running function's calls and arguments in a live system using :dbg or :recon_trace, without restarting it.
|
med |
| 35. Runtime introspection |
Inspect a live process's mailbox length, reduction count, and state using Process.info/1 and :observer.start.
|
low |
| 36. Schedulers | Spawn CPU-bound work, watch BEAM scheduler utilization, and move a blocking NIF onto a dirty scheduler to free schedulers. | high |
| 37. Hot code upgrade |
Change a running GenServer's state shape and migrate the old state to the new format inside code_change/3 with no downtime.
|
high |
| Phase 4 | Testing & Quality | |
| 38. ExUnit depth |
Write tests using setup, setup_all, on_exit, and tags, and learn which shared state breaks async: true.
|
low |
| 39. Doctests |
Write @doc examples that run as tests via doctest, keeping the documentation and the suite in sync.
|
low |
| 40. Deterministic race test |
Reliably reproduce a GenServer race in a test by controlling timing with a fake clock or a blocking message.
|
high |
| 41. Property testing |
Use StreamData to generate random inputs and assert a serializer's encode>decode round-trips, letting it shrink failures.
|
med |
| 42. Mox |
Define a behaviour for an external service and use Mox to set per-test expectations and verify the calls were made.
|
med |
| 43. Benchee |
Compare two implementations of a function with Benchee, reporting iterations/sec, memory, and reductions.
|
low |
| 44. Dialyzer |
Add @specs to a module, introduce a type mismatch, and run Dialyzer to catch it via success typing.
|
med |
| Phase 5 | Data, Parsing & Serialization | |
| 45. Binary header parsing | Parse the fixed header of a PNG, WAV, or ELF file purely with binary pattern matching on its byte fields. | med |
| 46. Frame protocol codec | Encode and decode messages in a length-prefixed binary framing format (4-byte length + payload) over a buffer. | med |
| 47. Compression | Implement run-length encoding and/or a Huffman coder-decoder that compresses and losslessly restores a binary. | high |
| 48. Hand-rolled JSON | Write a JSON tokenizer and recursive-descent parser by hand (no library) that turns a JSON string into Elixir terms. | med |
| 49. NimbleParsec grammar |
Define a NimbleParsec grammar for arithmetic expressions that respects operator precedence and parentheses.
|
med |
| 50. Safe config DSL |
Parse a small custom config-file format into Elixir data structures without using Code.eval_string.
|
med |
| 51. Streaming CSV |
Read a huge CSV with NimbleCSV, transform rows lazily, and write the result back out without loading the whole file.
|
low |
| 52. Mini changeset |
Build a small struct validator/caster (cast fields, check required, return errors) that mimics Ecto.Changeset without Ecto.
|
med |
| 53. Structural diff | Walk two nested maps/lists and produce a readable diff of what was added, removed, or changed. | med |
| Phase 6 | Networking & Protocols | |
| 54. TCP echo |
Write a :gen_tcp server that echoes back whatever a client sends, trying both active: true and passive recv modes.
|
med |
| 55. TCP chat | Build a TCP chat server that accepts many clients (one acceptor process each) and broadcasts each line to all the others. | med |
| 56. UDP heartbeat |
Receive periodic heartbeat/stat datagrams over :gen_udp and track which senders are still alive.
|
low |
| 57. Toy HTTP server |
Parse an HTTP/1.1 request line and headers off a raw :gen_tcp socket and return a valid response, no web framework.
|
high |
| 58. Req client |
Build a typed HTTP client with Req that adds retries with backoff, a custom request step, and streams large responses.
|
med |
| 59. Pooled HTTP |
Make pooled HTTP requests with Finch or Mint and benchmark throughput against a naive one-connection-per-request client.
|
med |
| 60. WebSocket client |
Connect to a public WebSocket feed with Mint.WebSocket, subscribe to a channel, and process incoming frames.
|
high |
| 61. SSE consumer |
Consume a Server-Sent-Events endpoint and expose the event stream as a lazy Elixir Stream.
|
med |
| Phase 7 | Distribution & Clustering | |
| 62. Manual clustering |
Start two named nodes, connect them with Node.connect/1, and run a function on the remote node via Node.spawn or :rpc.
|
med |
63. :pg pub/sub
|
Build a cross-node publish/subscribe system using Erlang's :pg process groups, with no external library.
|
med |
64. :global singleton
|
Register a single GenServer cluster-wide with :global so any node reaches the same process by name.
|
med |
| 65. libcluster |
Use libcluster's gossip strategy so nodes discover each other and form a cluster automatically on startup.
|
med |
| 66. Node lifecycle monitoring |
Subscribe with :net_kernel.monitor_nodes and rebalance or fail over work when nodes join or leave the cluster.
|
med |
67. :erpc multicall fan-out
|
Run a query on every node at once with :erpc.multicall/4, gather the results, and compare it to legacy :rpc.
|
med |
| 68. Distributed lock |
Serialize a critical section across the whole cluster using :global.trans or :global.set_lock.
|
med |
| 69. libcluster on Kubernetes |
Configure libcluster's Kubernetes strategy so pods form a cluster via headless-service DNS discovery.
|
med |
| 70. Distributed ID generator | Generate collision-free Snowflake-style IDs across nodes by encoding a per-node id, timestamp, and sequence. | med |
| 71. Nebulex distributed cache |
Set up a partitioned or replicated cache with Nebulex and broadcast invalidations so all nodes stay consistent.
|
med |
| 72. Leader election |
Elect a single leader among clustered nodes (bully algorithm or a :global lock) and re-elect when the leader dies.
|
high |
| 73. Work-stealing queue | Build a distributed task queue where idle nodes pull/steal work from busier nodes to balance load. | high |
| 74. DeltaCrdt convergent state |
Replicate a counter or set across nodes with DeltaCrdt so concurrent updates merge and converge without conflicts.
|
high |
| 75. Horde distributed processes |
Run a process under Horde.DynamicSupervisor + Horde.Registry so it survives a node dying and is handed off elsewhere.
|
high |
| 76. Mnesia KV |
Build a replicated key-value store with :mnesia ram_copies on two nodes, reading and writing inside transactions.
|
high |
| 77. Partition behavior |
Simulate a network split between nodes, then heal it, and observe how :global resolves the name conflicts.
|
high |
| 78. FLAME elastic compute |
Use FLAME to run a heavy function on a freshly spun-up ephemeral node and tear it down when the work finishes.
|
high |
| Phase 8 | Metaprogramming | |
| 79. AST spelunking |
Quote expressions like if, the capture operator, and pipelines, then Macro.to_string the AST to see how they desugar.
|
med |
| 80. Module introspection |
At runtime, list a module's functions, attributes, and behaviours using __info__/1 and the Module/Code APIs.
|
med |
81. assert_match macro
|
Write a macro that pattern-matches an expression and, on failure, prints both the expected pattern and the actual value. | med |
| 82. Custom sigils |
Define sigil_v and sigil_m so ~v"1.2.0" parses a version and ~m"4.99" parses money into a struct.
|
med |
83. __using__ mixin
|
Build a module that, when used, injects helper functions and registers a module attribute in the caller.
|
med |
| 84. Compile-time embedding |
Read a data file at compile time with @external_resource and embed its contents so there's no runtime file read.
|
med |
| 85. Codegen from data | Generate a set of functions at compile time by looping over a data list inside a macro (e.g., one function per status). | med |
| 86. Checked enum macro |
Write a macro that, from a list of atoms, generates an enum module with values/0, guards, and @specs.
|
high |
| 87. A tiny DSL |
Build a small block-style DSL (e.g., a validation or routing block) with defmacro that compiles to ordinary function calls.
|
high |
| Phase 9 | Numerical & ML Foundations | |
| 88. Nx basics |
Create Nx tensors, use broadcasting and defn, and compare a vectorized operation against the equivalent list comprehension.
|
med |
| 89. Gradient descent |
Implement linear regression from scratch in Nx.Defn, computing gradients and updating weights over epochs.
|
med |
| 90. Fractal renderer |
Compute a Mandelbrot or Julia set as an Nx tensor and write the escape-iteration values out as a PNG image.
|
med |
| 91. Explorer DataFrames |
Load a CSV into an Explorer DataFrame, group and aggregate columns, and compare the code to a hand-rolled Enum version.
|
low |
| 92. Scholar |
Run k-means clustering or k-NN classification on a small toy dataset using Scholar.
|
med |
| 93. Bumblebee embeddings |
Load a sentence-embedding model with Bumblebee and serve it locally through Nx.Serving to embed text.
|
med |
| 94. Batched inference |
Feed many inputs through an Nx.Serving so they're automatically batched for higher throughput.
|
med |
| 95. EXLA benchmark |
Run the same matrix multiply on the pure-Elixir BinaryBackend and the EXLA (XLA) backend and compare timings.
|
med |
| 96. XOR net |
Define, train, and evaluate a tiny feed-forward network that learns XOR using Axon.
|
med |
| Phase 10 | Tooling, NIFs & Releases | |
| 97. escript CLI |
Build a standalone command-line tool as an escript, parsing subcommands and flags with OptionParser and printing --help.
|
low |
| 98. Mix task |
Write a custom Mix.Task (e.g., mix my.gen) that inspects the project or generates a file.
|
low |
| 99. Owl TUI |
Build an interactive terminal UI with Owl featuring a live progress bar, a formatted table, and a prompt.
|
med |
| 100. File watcher |
Use the file_system library to watch a directory and re-run a command whenever a file changes.
|
med |
| 101. Ports |
Launch an external program via a Port, stream its stdout into Elixir, and shut it down cleanly.
|
med |
| 102. Rustler NIF |
Implement a CPU-bound function as a Rust NIF with Rustler and benchmark it against the pure-Elixir version.
|
high |
| 103. Zigler NIF |
Implement the same CPU-bound function as a Zig NIF with Zigler and compare ergonomics and speed to Rustler.
|
high |
| 104. Standalone telemetry |
Emit :telemetry events from a plain library function and attach a handler that aggregates and logs them.
|
med |
| 105. Profiling |
Profile a slow function with :eprof or :fprof, find the hot path, and optimize it.
|
med |
| 106. Release + runtime.exs |
Build a production release with mix release and configure it at boot from environment variables via runtime.exs.
|
med |
| 107. Burrito binary |
Package an Elixir CLI into a single self-contained native executable with Burrito.
|
med |
| 108. Custom build step |
Add a custom compiler or pre-compile step to a project through Mix aliases and the :compilers config.
|
high |
| Phase 11 | Pure-Elixir Synthesis | |
| 109. Actor Game of Life | Implement Conway's Game of Life with one process per cell messaging its neighbors, and watch patterns emerge from local rules. | high |
| 110. Lisp/Forth interpreter | Build a tokenizer, parser, and evaluator for a tiny Lisp or Forth that can run simple programs. | high |
| 111. Backtesting pipeline |
Stream historical OHLC bars through a GenStage pipeline that computes indicators (e.g., SMA, RSI) as they pass.
|
high |
| 112. Set-theoretic types | Write functions and read the warnings/inferences from Elixir's new set-theoretic type system to learn how it reasons. | med |
| PART II | PHOENIX & THE WEB LAYER | |
| Phase 12 | LiveView Core Mechanics | med |
| 113. Component diff study |
Build the same widget once as a function component and once as a LiveComponent, and compare the diffs sent over the wire.
|
low |
| 114. JS commands |
Build accordions and dropdowns using only Phoenix.LiveView.JS commands so they toggle with no server round-trip.
|
med |
115. assign_async dashboard
|
Load three independent dashboard widgets concurrently with assign_async, each showing its own loading/error/loaded state.
|
med |
116. start_async export
|
Kick off a long-running export with start_async and add a Cancel button that calls cancel_async and resets the UI.
|
med |
117. stream_async list
|
Load a large collection into a LiveView with stream_async so it streams in without ever sitting in assigns.
|
med |
| 118. Capped live log |
Show a live-tailing log that appends new lines via a LiveView stream and keeps only the most recent N with :limit.
|
med |
| 119. Form auto-recover |
Build a form that restores its in-progress values after a socket disconnect/reconnect using phx-auto-recover.
|
med |
| 120. Nested LiveViews |
Embed a child LiveView with live_render and sticky: true so it keeps its state across parent navigation.
|
med |
| 121. Direct-to-S3 upload | Upload a file straight to S3 from the browser using LiveView external uploads with presigned URLs and a progress bar. | high |
| 122. Multi-file drop upload | Accept multiple drag-and-dropped files in a LiveView upload and show client-side image previews before submitting. | med |
| Phase 13 | LiveView UI & JS Interop | |
| 123. Charting hook |
Render a Chart.js chart in a JS hook and update it live by pushing new data from the server with push_event.
|
med |
| 124. Scroll-aware header | Use a JS hook that throttles scroll events and tells the server when to restyle a sticky header (e.g., shrink on scroll). | med |
| 125. Toasts |
Build auto-dismissing, stackable flash/toast notifications animated with Phoenix.LiveView.JS transitions.
|
low |
| 126. Modal stack |
Support opening modals on top of modals, each with focus trapping, driven by JS.show/JS.hide.
|
med |
| 127. Lazy tabs |
Build a tab bar where each tab's LiveComponent only mounts and loads its data the first time it's opened.
|
low |
| 128. Typeahead |
Build an autocomplete input that does a debounced (phx-debounce) server search and supports keyboard selection of results.
|
med |
| 129. Inline-editable table | Let users click a table cell to edit it in place, saving on blur/enter with optimistic UI and rollback on error. | med |
| 130. Multi-step wizard | Build a multi-step form that validates each step and lets users move back and forward without losing entered data. | med |
| 131. Date-range picker |
Build a stateful LiveComponent date-range picker with month navigation and start/end selection.
|
med |
| 132. Color picker |
Build a color picker that previews live and writes the chosen value into CSS custom properties via push_event.
|
low |
| 133. Tree explorer | Build a collapsible file/folder tree whose child nodes load lazily the first time a node is expanded. | med |
| 134. Infinite scroll |
Page additional items into a LiveView stream when the user scrolls to the bottom, using phx-viewport-bottom.
|
med |
| 135. Resizable data grid | Build a data grid with server-side column sorting and a JS hook for drag-to-resize columns that persists widths. | high |
| 136. Drag-drop reorder | Make a list reorderable with a Sortable.js hook, pushing the new order to the server and persisting positions. | med |
| 137. Kanban board | Build a board where cards drag between columns (Sortable.js groups), backed by LiveView streams to keep diffs small. | high |
| 138. Image cropper | Crop an image client-side with Cropper.js in a hook, then upload the resulting cropped blob. | high |
| 139. Markdown editor | Build a Markdown editor that renders a live HTML preview on each keystroke using Earmark. | low |
| 140. Block editor | Build a Notion-style block editor with TipTap/ProseMirror in a hook and sync the document to the server as structured blocks. | high |
| 141. Command palette |
Build a ⌘K command palette modal with fuzzy search and full keyboard navigation via phx-keydown.
|
med |
| Phase 14 | Real-time & Collaboration | |
| 142. Server ticker |
Run a GenServer that broadcasts on an interval over PubSub and have a LiveView render each tick as it arrives.
|
low |
| 143. Raw Channel |
Build a Phoenix Channel with a hand-written JavaScript client (no LiveView) to sync multiplayer cursors.
|
med |
| 144. Presence online list |
Show a live "who's online" list using Phoenix.Presence, updating as users join and leave.
|
med |
| 145. Chat typing indicators | Build a chat room over PubSub that broadcasts ephemeral "user is typing" state separate from sent messages. | med |
| 146. Live poll | Build a poll whose result bars animate live for all viewers as votes arrive over PubSub. | low |
| 147. Live cursors | Broadcast each user's mouse position over PubSub so everyone sees the others' cursors moving in real time. | med |
| 148. Selection sync |
Sync each user's text cursor and selection range in a shared field across clients using Phoenix.Presence metas.
|
high |
| 149. Distributed PubSub |
Run two clustered nodes and confirm a Phoenix.PubSub broadcast on one node reaches a LiveView mounted on the other.
|
high |
| 150. GenStage in LiveView |
Visualize a GenStage pipeline's backpressure live, showing demand and buffer sizes update in a LiveView.
|
high |
| 151. Phoenix.Sync |
Sync a Postgres table directly into a LiveView (and a plain JS client) in real time with Phoenix.Sync/Electric.
|
high |
| Phase 15 | Persistence with Ecto | |
| 152. Ecto.Multi transfer |
Move money between two accounts in one Ecto.Multi transaction so it fully succeeds or rolls back, handling constraint errors.
|
med |
| 153. Composable queries |
Build a function that composes an Ecto query from a filter map, adding where clauses only for the params present.
|
med |
| 154. Custom Ecto.Type |
Define a custom Ecto.Type that stores money as integer cents, plus an Ecto.Enum field for status.
|
med |
| 155. Embedded schema |
Store a validated settings blob in a JSONB column using an embedded schema and cast_embed.
|
med |
| 156. Optimistic locking |
Add a lock_version field and use Ecto's optimistic locking to reject stale concurrent updates.
|
med |
| 157. Soft delete |
Implement soft deletes with a deleted_at column and a query helper that excludes deleted rows by default.
|
low |
| 158. Polymorphic assoc | Associate one resource (e.g., comments) with several parent types the idiomatic Ecto way, using separate join tables. | med |
| 159. Window functions |
Write an Ecto query using a SQL window function (via fragment/over) to rank rows within groups for a leaderboard.
|
high |
| 160. Full-text search |
Add a Postgres tsvector column with a GIN index and run ranked full-text search queries through Ecto.
|
med |
| 161. Materialized view | Create and refresh a Postgres materialized view in a migration and query it through a read-only Ecto schema. | med |
| 162. Streaming export |
Export a huge result set with Repo.stream/2 inside a transaction, writing rows out without loading them all into memory.
|
med |
| Phase 16 | Background Work & Pipelines | |
| 163. Oban basics |
Define an Oban worker that retries with backoff on failure and uses a uniqueness key to avoid duplicate jobs.
|
low |
| 164. Oban cron |
Schedule a recurring digest job with Oban's Cron plugin to run on a fixed daily schedule.
|
low |
| 165. Oban fan-out + join |
Enqueue a large batch of Oban jobs and run a single callback once the entire batch has finished.
|
high |
| 166. Rate-limited queue |
Configure an Oban queue that respects a global rate limit when calling a throttled external API.
|
med |
| 167. Oban workflow |
Chain dependent Oban jobs so a job only runs after its prerequisites complete.
|
med |
| 168. Broadway pipeline |
Build a Broadway pipeline that consumes from a producer (e.g., SQS/in-memory) with batching and bounded concurrency.
|
med |
| Phase 17 | Auth, Scopes & Security | |
| 169. Multi-tenant scopes | Use Phoenix 1.8 scopes to enforce per-tenant data access and thread the scope through a context and its LiveViews. | med |
| 170. Role-based authz |
Enforce role-based access with a plug for controllers and an on_mount hook for LiveViews.
|
med |
| 171. OAuth social login |
Add "Sign in with Google/GitHub" to a phx.gen.auth app using Assent (or Ueberauth), handling the provider callback and linking the external identity to a user account.
|
med |
| 172. API tokens |
Issue and verify API bearer tokens with Phoenix.Token behind an authentication plug pipeline.
|
med |
| 173. Login rate limiting |
Throttle repeated login attempts on an endpoint using Hammer to slow brute-force attacks.
|
low |
| 174. Cookie tampering | Show how Phoenix's signed/encrypted session cookies detect tampering by editing a cookie and observing rejection. | med |
| 175. Passkey login |
Implement WebAuthn passkey registration and login using a wax-based library.
|
high |
| Phase 18 | APIs & Integration | |
| 176. Webhook receiver | Build an endpoint that verifies an incoming webhook's HMAC signature (Stripe-style) and rejects replays before processing. | med |
| 177. JSON:API endpoint | Expose a resource as a JSON:API endpoint with proper content negotiation and structured error envelopes. | med |
| 178. GraphQL + Dataloader |
Build an Absinthe GraphQL schema that batches association loads with Dataloader to avoid N+1 queries.
|
med |
| 179. GraphQL subscriptions |
Push live updates to GraphQL clients over a socket using Absinthe subscriptions.
|
high |
| Phase 19 | Ash Framework | |
| 180. Calculations + aggregates |
Define an Ash resource with calculations and aggregates and surface those computed fields in a LiveView table.
|
med |
181. Nested AshPhoenix.Form
|
Build an AshPhoenix.Form for a parent and its has_many children with add/remove-row controls.
|
med |
| 182. Field-level policies |
Add Ash policies that restrict access to specific fields and write tests proving the denials work.
|
high |
| 183. AshOban trigger |
Use AshOban to fire a background action automatically when a record changes state.
|
med |
| 184. AshAuthentication |
Stand up AshAuthentication with password and magic-link strategies from scratch.
|
med |
| Phase 20 | AI / LLM In the App | |
| 185. ReqLLM streaming chat |
Build a chat LiveView that streams an LLM's tokens into the UI as they arrive using ReqLLM.
|
med |
| 186. Tool calling |
Let an LLM call an Elixir function via ReqLLM tool calling and render the function's result back in the chat.
|
high |
| 187. Instructor extraction |
Use Instructor to extract structured, schema-validated data (an Ecto struct) from freeform text.
|
med |
| 188. Semantic search |
Store embeddings in pgvector and rank results by cosine distance for a semantic search box over a small corpus.
|
med |
| 189. RAG over notes |
Build retrieval-augmented Q&A: embed notes into pgvector, retrieve the closest chunks, and feed them to the LLM.
|
high |
| 190. Zero-shot image tags |
Classify uploaded images with a zero-shot Bumblebee model behind a LiveView upload.
|
med |
| 191. Whisper transcription |
Transcribe a recorded audio clip to text with Whisper via Bumblebee from a LiveView mic upload.
|
high |
| 192. Visible agent steps |
Build a simple agent loop that streams its intermediate reasoning/actions into the UI with stream_async.
|
high |
| 193. Tidewave MCP |
Connect the Tidewave MCP server to an AI editor so it can read your running app's logs, schemas, and state.
|
med |
| Phase 21 | Testing the Web Layer | |
| 194. LiveViewTest depth |
Write LiveViewTest assertions covering streamed inserts/updates, form validation errors, and push_event payloads.
|
med |
| 195. PhoenixTest flow |
Write one user-journey test with PhoenixTest that runs unchanged across both dead and live views.
|
med |
| 196. Browser test |
Run that same flow in a real headless browser with phoenix_test_playwright.
|
med |
| Phase 22 | Observability & Deployment | |
| 197. Structured logging |
Emit JSON-formatted logs enriched with request metadata via a custom Logger formatter/backend.
|
low |
| 198. Telemetry → LiveDashboard |
Emit custom :telemetry events from a context and chart them on a LiveDashboard page.
|
med |
| 199. Custom LiveDashboard page |
Add a custom LiveDashboard page that displays a GenServer's live internal state.
|
med |
| 200. Minimal release |
Build a mix release served by Bandit, run it in a slim Docker image, and add a /healthz endpoint.
|
med |