LiveView and PWA
Introduction A demo of a real-time, collaborative multi-page web app built with Phoenix LiveView. It is designed for offline-first ready; it is packaged as a PWA and uses CRDTs or local state and reactive components. Offline first solutions naturally offloads most of the reactive UI logic to JavaScript. When online, we use LiveView "hooks", while when offline, we render the reactive components. It uses Vite as the bundler. GitHub: Source code We can check it deployed: https://solidyjs-lively-pine-4375.fly.dev/ or Context We want to experiment PWA webapps using Phoenix LiveView. We building a simple two pages webapp: LiveStock. On the first page, we mimic a shopping cart where users can pick items until stock is depleted, at which point the stock is replenished. Every user will see and can interact with this counter LiveFlight. On the second page, we propose an interactive map with a form with two inputs where two users can edit collaboratively a form to display markers on the map and then draw a great circle between the two points. Tech overview Component Role Vite Build and bundling framework SQLite Embedded persistent storage of latest Yjs document Phoenix LiveView UI rendering, incuding hooks PubSub / Phoenix.Channel Broadcast/notifies other clients of updates / conveys CRDTs binaries on a separate websocket (from te LiveSocket) Yjs / Y.Map Holds the CRDT state client-side (shared) Valtio Holds local ephemeral state y-indexeddb Persists state locally for offline mode SolidJS renders reactive UI using signals, driven by Yjs observers Hooks Injects communication primitives and controls JavaScript code Service Worker / Cache API Enable offline UI rendering and navigation by caching HTML pages and static assets Leaflet Map rendering MapTiler enable vector tiles WebAssembly container high-performance calculations for map "great-circle" routes use Zig code compiled to WASM LiveStock page You have both CRDT-based synchronization (for convergence) and server-enforced business rules (for consistency). We thus have two Layers of Authority: CRDT Sync Layer (Collaborative): Clients and server synchronize using Yjs CRDTs to merge concurrent edits deterministically. Clients can modify their local Y-Doc freely (offline or online). Business Rules Layer (Authoritative): The server is authoritative. It validates updates upon the business logic (e.g., stock validation), and broadcasts the canonical state to all clients. Clients propose changes, but the server decides the final state (e.g., enforcing stock limits). Implementation highlights Offline capabilities: Edits are saved to y-indexeddb and sent later. Synchronization Flow: Client sends all pending Yjs updates on (re)connection. The client updates his local Y-Doc with the server responses. Y-Doc mutations trigger UI rendering, and reciprocally, UI modifications update the Y-Doc and propagate mutations to the server. Server Processing: Merges updates into the SQLite3-stored Y-Doc (using y_ex). Applies business rules (e.g., "stock cannot be negative").Broadcasts the approved state. Clients reconcile local state with the server's authoritative version Data Transport: for the LiveStock page: Use a Phoenix.Channel to transmit the Y-Doc state as binary. This minimises the bandwidth usage and decouples CRDT synchronisation from the LiveSocket. The implementation heavily inspired by the repo https://github.com/satoren/y-phoenix-channel made by the author of y_ex. for the LiveFlight: use LiveSocket as we broadcast a limited amount of data. Component Rendering Strategy: online: use LiveView hooks offline: hydrate the cached HTML documents with reactive JavaScript components Service worker lifecyle When the client code is updated, we get notified: Once we accept the update, the new code is active: Build tool: Vite Since we need to setup a Service Worker, we used Vite and the plugin VitePWA. We let Vite bundle all the code and can remove safely esbuild and tailwindcss. They are now part of Vite. Source code of "vite.config.js" Store managers For the LiveStock page, we used Yjs client-side and y_ex server-side. For the LiveFlight page, we use Valtio as we didn't design the state of this page to survive a network disruption.

Introduction
A demo of a real-time, collaborative multi-page web app built with Phoenix LiveView
.
It is designed for offline-first ready; it is packaged as a PWA and uses CRDTs or local state and reactive components.
Offline first solutions naturally offloads most of the reactive UI logic to JavaScript
.
When online, we use LiveView
"hooks", while when offline, we render the reactive components.
It uses Vite
as the bundler.
GitHub: Source code
We can check it deployed: https://solidyjs-lively-pine-4375.fly.dev/ or
Context
We want to experiment PWA webapps using Phoenix LiveView.
We building a simple two pages webapp:
- LiveStock. On the first page, we mimic a shopping cart where users can pick items until stock is depleted, at which point the stock is replenished. Every user will see and can interact with this counter
- LiveFlight. On the second page, we propose an interactive map with a form with two inputs where two users can edit collaboratively a form to display markers on the map and then draw a great circle between the two points.
Tech overview
Component | Role |
---|---|
Vite | Build and bundling framework |
SQLite | Embedded persistent storage of latest Yjs document |
Phoenix LiveView | UI rendering, incuding hooks |
PubSub / Phoenix.Channel | Broadcast/notifies other clients of updates / conveys CRDTs binaries on a separate websocket (from te LiveSocket) |
Yjs / Y.Map | Holds the CRDT state client-side (shared) |
Valtio | Holds local ephemeral state |
y-indexeddb | Persists state locally for offline mode |
SolidJS | renders reactive UI using signals, driven by Yjs observers |
Hooks | Injects communication primitives and controls JavaScript code |
Service Worker / Cache API | Enable offline UI rendering and navigation by caching HTML pages and static assets |
Leaflet | Map rendering |
MapTiler | enable vector tiles |
WebAssembly container | high-performance calculations for map "great-circle" routes use Zig code compiled to WASM
|
LiveStock page
You have both CRDT-based synchronization (for convergence) and server-enforced business rules (for consistency).
We thus have two Layers of Authority:
CRDT Sync Layer (Collaborative):
Clients and server synchronize using Yjs CRDTs to merge concurrent edits deterministically.
Clients can modify their local Y-Doc freely (offline or online).Business Rules Layer (Authoritative):
The server is authoritative. It validates updates upon the business logic (e.g., stock validation), and broadcasts the canonical state to all clients.
Clients propose changes, but the server decides the final state (e.g., enforcing stock limits).
Implementation highlights
Offline capabilities:
Edits are saved toy-indexeddb
and sent later.Synchronization Flow:
Client sends all pendingYjs
updates on (re)connection.
The client updates his localY-Doc
with the server responses.
Y-Doc
mutations trigger UI rendering, and reciprocally, UI modifications update theY-Doc
and propagate mutations to the server.Server Processing:
Merges updates into theSQLite3
-storedY-Doc
(usingy_ex
).
Applies business rules (e.g., "stock cannot be negative").Broadcasts the approved state.
Clients reconcile local state with the server's authoritative version-
Data Transport:
- for the LiveStock page: Use a
Phoenix.Channel
to transmit theY-Doc
state as binary. This minimises the bandwidth usage and decouples CRDT synchronisation from the LiveSocket. The implementation heavily inspired by the repo https://github.com/satoren/y-phoenix-channel made by the author ofy_ex
. - for the LiveFlight: use
LiveSocket
as we broadcast a limited amount of data.
- for the LiveStock page: Use a
-
Component Rendering Strategy:
- online: use LiveView hooks
- offline: hydrate the cached HTML documents with reactive JavaScript components
Service worker lifecyle
When the client code is updated, we get notified:
Once we accept the update, the new code is active:
Build tool: Vite
Since we need to setup a Service Worker, we used Vite
and the plugin VitePWA
.
We let Vite
bundle all the code and can remove safely esbuild
and tailwindcss
. They are now part of Vite
.
Source code of "vite.config.js"
Store managers
For the LiveStock page, we used Yjs
client-side and y_ex server-side.
For the LiveFlight page, we use Valtio
as we didn't design the state of this page to survive a network disruption.