Tutorial for PETALS stack (Elixir, Phoenix, Tailwind, Ash, Live View, and Svelte)
| Layer | Technology | Primary Role in PETALS |
|---|---|---|
| Runtime | Elixir | Handles concurrency, fault tolerance, and soft real-time execution via the Erlang BEAM VM. |
| Web Framework | Phoenix | Provides routing, WebSocket channels, HTTP handling, and high-throughput web performance. |
| Styling | Tailwind CSS | Offers rapid utility-first styling consistent across LiveView templates and Svelte components. |
| Domain & Data | Ash Framework | Declaratively defines data resources, business logic, authorization policies, and auto-generated APIs. |
| Server-Driven UI | Phoenix LiveView | Manages real-time state and UI reactivity on the server over WebSockets, bypassing traditional APIs. |
| Client-Driven UI | Svelte | Handles complex, highly interactive, client-side UI components where server round-trips are impractical. |
Elixir runs on the Erlang VM which has many examples of scalability
- Ericson, the company that created Erlang, boasts only 5.2 minutes of downtime a year in their systems running Erlang.
- Whatsapp was able to maintain 2 million TCP connections on a single mid-tier server, using only 40% CPU, in 2012 using Erlang. Source
- A developer rewrote an AWS microservice application in Elixir. The resulting application was faster and cost less (from >$12K/month to ~$300/month) to run in the cloud. Source
- Discord has many examples of using Elixir as thier primay language to scale to a large amount of users. Source
Elixir was created by former Ruby/Rails devs that built it with improvements over Erlang
- Modern Ruby-like syntax
- Macro System to support metaprogramming and domain-specific languages
- Tooling (
mixbuild tool and hex package manager) - Many libraries and frameworks
Database migrations, schema definitions, context functions, GraphQL/REST endpoint controllers, authorization checks and more are centralized and declared.
- Declarative Resources: You define your data and business rules once as Ash Resources.
- Auto-Derived Interfaces: Ash automatically derives persistence rules, validation, background jobs, and GraphQL or JSON:API endpoints directly from your resource definitions.
- Built-in Security & Policies: Granular policy-based access control (RBAC/ABAC) is baked into the domain layer, ensuring security rules are enforced regardless of how data is accessed (LiveView, API, or background job).
Traditional full-stack apps require maintaining a separate REST/GraphQL API layer, complex client-side state management (like Redux), and data-fetching boilerplate.
- LiveView Handles 90% of UI: Server-driven state means state management happens in one place—on the server. State changes trigger minimal HTML diffs pushed instantly over WebSockets.
- No Manual Serialization: You pass server data directly into UI components without writing client-side fetchers, TypeScript interfaces for API payloads, or cache-invalidation logic.
Pure LiveView is good for standard forms, dashboards, tables, and CRUD operations. However it becomes complex during client-side UI interactions.
live_svelteBridge: Through libraries likelive_svelte, Svelte components render seamlessly inside Phoenix LiveView templates.- Reactive Synergy: LiveView owns the application state, database connection, and real-time synchronization, while Svelte handles complex local DOM manipulations. Svelte receives props directly from LiveView and sends events back up to the server with zero manual API wiring.
- Minimal JavaScript Footprint: Svelte compiles to small, efficient JavaScript without a virtual DOM, preserving the fast initial load times Elixir/Phoenix is known for.
Because PETALS runs on Elixir and the Erlang BEAM you get world-class concurrency for free.
- Massive Concurrency: A single Phoenix server can maintain hundreds of thousands of simultaneous WebSocket connections with low memory usage.
- Self-Healing Systems: Built-in OTP supervisors isolate errors. If an individual WebSocket process or background task crashes, it restarts instantly without bringing down the server or impacting other users.
- Built-in Pub/Sub: Real-time features (collaborative editing, live notifications, dynamic feeds) require no external services like Redis or Socket.io.
PETALS scales both technically and organizationally.
- Monolith First, Microservice Ready: Ash resources decouple domain logic from persistence. If a module needs to be extracted into a separate service later, Ash resources can switch storage backends or execution boundaries without rewriting business logic.
- Low Maintenance Load: Fewer infrastructure moving parts (no separate Node/React build pipeline, microservice API gateways, or external state caches) means a small team can manage a platform serving millions of users.
Install Elixir
# install asdf (a tool for managing runtimes)
# Getting started: https://asdf-vm.com/guide/getting-started.html
# install erlang and elixir plugins
asdf plugin add erlang
asdf plugin add elixir
# install specific version for plugin
# use `asdf list all` or `asdf latest` to check for newer available versions
asdf install erlang 29.0.5
asdf install elixir 1.20.3-otp-29Create a new Phoenix Project
# install/update Hex (Elixir package manager, like npm or pip)
mix local.hex --force
# install Phoenix creation scripts
mix archive.install hex phx_new --force
mix archive.install hex igniter_new --force
# create a new app
mix igniter.new petals_stack_tutorial --with phx.new --install ash,ash_phoenix,ash_postgres,live_svelte --yes
# go to your new app
cd petals_stack_tutorial/
# set elixir versions, this will create a .tool-versions file
asdf set erlang 29.0.2
asdf set elixir 1.20.2-otp-29Run the database
# run Postgres via Docker
docker run -d \
--name phx_db \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_HOST_AUTH_METHOD=trust \
-p 5432:5432 \
postgresRun the server
# install dependencies, run migrations
mix setup
# run the server
mix phx.server
# run the server and a console
iex -S mix phx.serverOptional, install additional packages (see more at https://ash-hq.org/#get-started)
mix igniter.install PACKAGE1 PACKAGE2 PACKAGE3Here are some addtional packages I recommend:
ash_authentication: An extension for the Ash Framework providing turnkey user authentication, including password strategies, OAuth2, and token management.ash_json_api: An Ash extension that automatically exposes your resources as a JSON:API-compliant REST interface.ash_graphql: An Ash extension that generates Absinthe-powered GraphQL schemas and endpoints directly from your resources.ash_admin: An automatically generated administration UI for viewing, filtering, and mutating data across your Ash resources.ash_ai: A framework extension that brings LLM capabilities to Ash, supporting prompt-backed actions, vector embeddings, and Model Context Protocol (MCP) integrations.live_debugger: A browser-based development tool for inspecting component trees, viewing assigns, and tracing callback executions in Phoenix LiveView applications.tidewave: An AI development toolkit by Dashbit that integrates runtime introspection, browser automation, and point-and-click UI tracing directly into Phoenix.
In traditional Model-View-Controller architecture: HTTP requests are made to the server, HTML is rendered server-side, and returned without state.
In Phoenix LiveView: the initial HTTP request renders a static page quickly, then creates a WebSocket connection to update the client with the state on the server.
We'll make a simple counter example.
First we need ot add the LiveView to the router.
scope "/", PetalsStackTutorialWeb do
pipe_through :browser
# new example scope
scope "/examples" do
# new counter example
live "/counter", CounterLive
end
endThis will put our Live View at /examples/counter
Next ceate the PetalsStackTutorialWeb.CounterLive module in file lib/petals_stack_tutorial_web/live/counter_live.ex
defmodule PetalsStackTutorialWeb.CounterLive do
use PetalsStackTutorialWeb, :live_view
endNext we need to implment 3 callbacks
mount/3which is called when the WebSocket is mounted. Used to initial the view.render/1renders and returns the HTML.handle_event/3handles events emmited from the view.
For mount, we assign the value of "counter" to 0 in the socket.
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, counter: 0)}
endNext, return the view. For now we'll keep it inline as a string, but we could use a HEEx template.
@impl true
def render(assigns) do
~H"""
<div>
<h1>Counter is {@counter}</h1>
<button phx-click="inc" phx-debounce="20">+</button>
<button phx-click="dec" phx-debounce="20">-</button>
</div>
"""
endThe counter assignment is extracted from assigns with the "@" macro. The phx-click attribute sets the name of the event that is sent to the server when the button is clicked. The phx-debounce="20" attribute setting ensures the events debounce for 20 milliseconds.
Finally, handle the events.
@impl true
def handle_event("inc", _payload, socket) do
{:noreply, update(socket, :counter, fn x -> x + 1 end)}
end
def handle_event("dec", _payload, socket) do
f = fn
0 -> 0
x -> x - 1
end
{:noreply, update(socket, :counter, f)}
endWe can lean on Elixir's pattern matching here. handle_event/3 has a seperate clause for each event, and the decrement event function handles when trying to decrement 0. We use update/3 function to pass a function and update the assignment, rather than reading the value and updating it.
The result is a view that is declaritive and only changed via state that is stored and changed server side.