How Phoenix PubSub Enables Real-Time Web Updates in TeslaMate: Event-Driven Architecture Explained
TeslaMate uses Phoenix PubSub as a centralized message bus that decouples backend state machines from frontend LiveViews, enabling instantaneous push-based UI updates without page reloads.
TeslaMate is an open-source data logger and visualization tool for Tesla vehicles, built with Elixir and Phoenix. Real-time communication between the vehicle state machines, MQTT handlers, and the web interface relies entirely on Phoenix PubSub, a distributed publish-subscribe system. This architecture ensures that whenever vehicle data changes, settings update, or import processes progress, the web interface reflects those changes immediately.
Initializing the PubSub Infrastructure
The PubSub system is established early in the application lifecycle. In lib/teslamate/application.ex, the supervision tree starts a Phoenix.PubSub process registered under the name TeslaMate.PubSub:
children = [
# ... other workers
{Phoenix.PubSub, name: TeslaMate.PubSub},
# ...
]
This single process acts as the message broker for the entire application, allowing any process to broadcast messages to topics and any other process to subscribe to them. By placing PubSub in the supervision tree, TeslaMate ensures that the message bus restarts automatically if it crashes, maintaining system resilience.
Publishing Events from Backend Processes
Backend components publish events through the PubSub bus whenever state changes occur. This decouples data producers from consumers, allowing the vehicle state machines to operate independently of the web layer.
Vehicle State Machine Broadcasts
The TeslaMate.Vehicles.Vehicle GenStateMachine manages individual vehicle connections and publishes two primary message types. After each state transition, broadcast_summary/1 in lib/teslamate/vehicles/vehicle.ex constructs a payload containing the current state, timestamps, health status, and geofence information, then broadcasts it:
defp broadcast_summary(state, %Data{car: car, last_response: vehicle} = data) do
payload = Summary.into(vehicle, %{
state: state,
since: data.last_state_change,
healthy?: healthy?(car.id),
elevation: data.elevation,
geofence: data.geofence,
car: car
})
call(data.deps.pubsub, :broadcast, [
TeslaMate.PubSub,
summary_topic(car.id),
payload
])
end
The function uses Phoenix.PubSub.broadcast/3 to send the message to a topic specific to that car ID (summary_topic(car.id)), ensuring that only subscribers interested in that particular vehicle receive the update.
Additionally, when the system initiates or completes data fetches from Tesla's API, broadcast_fetch/2 publishes status updates:
def handle_event(:internal, {:broadcast_fetch, status}, _state, data) do
call(data.deps.pubsub, :broadcast, [
TeslaMate.PubSub,
fetch_topic(data.car.id),
{:status, status}
])
end
This allows the UI to display loading indicators or "syncing" states in real time.
Settings Changes
The TeslaMate.Settings module in lib/teslamate/settings.ex publishes notifications when vehicle configurations change. The subscribe_to_changes/1 function and its corresponding broadcast mechanism use PubSub to alert LiveViews that settings such as sleep mode preferences or unit preferences have been modified:
def subscribe_to_changes(car_id) do
Phoenix.PubSub.subscribe(TeslaMate.PubSub, topic(car_id))
end
MQTT Integration Bridge
TeslaMate's MQTT subsystem also participates in the PubSub ecosystem. The TeslaMate.Mqtt.PubSub.VehicleSubscriber module (located in lib/teslamate/mqtt/pubsub/vehicle_subscriber.ex) subscribes to vehicle summary topics and forwards relevant data to MQTT brokers. This creates a bidirectional flow where MQTT messages can trigger PubSub events and vice versa, ensuring consistency across all integration points.
Subscribing to Topics in LiveViews
Frontend components consume PubSub messages through Phoenix LiveView subscriptions. Rather than polling for updates, LiveViews register themselves as subscribers during the mount phase.
In lib/teslamate_web/live/car_live/summary.ex, the LiveView establishes subscriptions to both summary and fetch topics for the specific car being displayed:
defmodule TeslaMateWeb.Live.CarLive.Summary do
use TeslaMateWeb, :live_view
alias TeslaMate.Vehicles
@impl true
def mount(_params, _session, socket) do
car = socket.assigns.car
:ok = Vehicles.subscribe_to_summary(car.id)
:ok = Vehicles.subscribe_to_fetch(car.id)
{:ok, assign(socket, :summary, nil)}
end
# ...
end
These calls delegate to the public API in lib/teslamate/vehicles.ex, which wraps the PubSub subscribe calls. Similarly, the import progress UI in lib/teslamate_web/live/import_live/index.ex subscribes to import-specific topics via TeslaMate.Import.subscribe/0, allowing users to watch import completion status in real time.
Handling Messages and Rendering Updates
Once subscribed, LiveViews implement handle_info/2 callbacks to process incoming PubSub messages. When a message arrives, the LiveView updates its socket assigns, triggering a diff that Phoenix LiveView pushes to the browser via WebSocket.
For example, TeslaMateWeb.Live.CarLive.Summary handles both summary updates and fetch status changes:
@impl true
def handle_info({:summary, summary}, socket) do
{:noreply, assign(socket, :summary, summary)}
end
@impl true
def handle_info({:status, fetching?}, socket) do
{:noreply, assign(socket, :fetching, fetching?)}
end
Because PubSub guarantees at-most-once delivery with low latency, these updates propagate to connected browsers within milliseconds of the backend state changing. The architecture supports horizontal scaling—multiple TeslaMate instances can share PubSub messages across nodes when configured with the Redis or PG2 adapters, though the default installation uses the single-node PG (Process Groups) adapter.
Summary
- Centralized message bus:
TeslaMate.PubSubruns as a supervised process inlib/teslamate/application.ex, providing the backbone for all real-time communication. - Topic-based routing: Vehicle-specific topics (created via
summary_topic/1andfetch_topic/1) ensure that updates reach only relevant subscribers, preventing broadcast storms. - Decoupled architecture: The
TeslaMate.Vehicles.VehicleGenStateMachine publishes events without knowing which LiveViews (if any) are listening, enabling independent scaling of backend and frontend components. - LiveView integration: Subscriptions occur during
mount/3in LiveViews likelib/teslamate_web/live/car_live/summary.ex, withhandle_info/2callbacks updating the UI instantly when messages arrive. - MQTT bridge: The PubSub system extends beyond the web interface to include MQTT subscribers, creating a unified event layer across all TeslaMate integrations.
Frequently Asked Questions
How does TeslaMate ensure that only specific car dashboards receive updates?
TeslaMate uses topic scoping in the PubSub system. Functions like summary_topic(car_id) and fetch_topic(car_id) in lib/teslamate/vehicles/vehicle.ex generate unique topic strings for each vehicle. When a LiveView mounts, it subscribes only to topics matching the current car.id, ensuring that the browser receives updates exclusively for the vehicle being viewed, rather than all vehicles in the system.
What happens to PubSub messages if the LiveView process crashes?
Phoenix PubSub maintains the subscription registry separately from the subscriber processes. If a LiveView crashes and restarts, it must re-subscribe during its new mount/3 call. Missed messages during the brief downtime are not replayed (PubSub provides at-most-once delivery), but the LiveView immediately receives the next broadcast, quickly resynchronizing with the current vehicle state.
Can TeslaMate scale across multiple servers using this PubSub architecture?
Yes. While the default configuration uses the PG (Process Groups) adapter suitable for single-node deployments, Phoenix PubSub supports distributed adapters like Redis or the built-in Phoenix.PubSub.PG2. By switching the adapter in lib/teslamate/application.ex, multiple TeslaMate nodes can share PubSub messages across the cluster, maintaining real-time updates for users regardless of which server handles their WebSocket connection.
How does the import process communicate progress to the web interface?
The import subsystem uses the same PubSub infrastructure as the vehicle state machines. In lib/teslamate_web/live/import_live/index.ex, the LiveView calls TeslaMate.Import.subscribe/0 during initialization, which registers it for import-specific topics. As the import worker processes Tesla data files, it broadcasts progress messages through TeslaMate.PubSub, allowing the LiveView to render progress bars and completion status without polling the database.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →