Back to projects

Real-Time Counter Dashboard with Phoenix LiveView

/ April 8, 2025

Introduction

As part of my journey to master Phoenix LiveView, I committed to building one project each day for a week. My first project was a Real-Time Counter Dashboard—a simple yet powerful application that demonstrates the core capabilities of LiveView. In this post, I'll walk through what I built, how I built it, and why it matters.

Audience, Goals, and Benefits

Audience

  • Elixir/Phoenix developers looking to explore LiveView
  • Web developers interested in real-time applications without heavy JavaScript
  • Development teams evaluating technology stacks for interactive web applications

Goals

  1. Demonstrate LiveView's real-time capabilities through a practical project
  2. Document the development and deployment process to Google Cloud Run
  3. Create a reference implementation that showcases Phoenix's server-side rendering approach to reactive UIs

Benefits

  1. For Developers: Clear example of LiveView's capabilities with minimal complexity
  2. For Teams: Insight into a full development cycle from concept to deployment
  3. For the Community: Practical knowledge sharing that bridges theory and practice

The Project: Real-Time Counter Dashboard

I built a dashboard of counters that update in real-time across all connected clients. Each counter maintains its state on the server and broadcasts changes to all connected clients—no additional JavaScript required.

Counter Dashboard Screenshot

Key Features

  • Multiple Independent Counters: Users can create, name, and manage multiple counters
  • Real-Time Updates: All changes appear instantly on all connected clients
  • Visual Feedback: Counters change color based on value and animate on updates
  • Persistence: Counter values persist through page reloads
  • Dashboard Statistics: Real-time calculation of stats across all counters

The Technology Stack

  • Frontend & Backend: Phoenix Framework with LiveView
  • Database: PostgreSQL for persistence
  • Deployment: Docker containerization deployed to Google Cloud Run
  • CI/CD: CircleCI for automated testing and deployment

Implementation Highlights

Phoenix LiveView Setup

The core of the application is a LiveView module that handles the state and events:

defmodule CounterDashboardWeb.CounterLive do
  use CounterDashboardWeb, :live_view
  alias CounterDashboard.Counters
  alias CounterDashboard.Counters.Counter

  def mount(_params, _session, socket) do
    if connected?(socket), do: Phoenix.PubSub.subscribe(CounterDashboard.PubSub, "counters")
    
    counters = Counters.list_counters()
    {:ok, assign(socket, counters: counters, new_counter_name: "", stats: calculate_stats(counters))}
  end

  # Event handlers and other functions
  # ...
end

Real-Time Communication

A key aspect of the project is how events propagate across clients. When a user increments a counter, we:

  1. Update the database
  2. Broadcast the change via PubSub
  3. Update the UI on all connected clients
def handle_event("increment", %{"id" => id}, socket) do
  counter = Counters.get_counter!(id)
  {:ok, updated_counter} = Counters.update_counter(counter, %{value: counter.value + 1})
  Phoenix.PubSub.broadcast(CounterDashboard.PubSub, "counters", {:counter_updated, updated_counter})
  
  {:noreply, socket}
end

def handle_info({:counter_updated, counter}, socket) do
  counters = update_counter_in_list(socket.assigns.counters, counter)
  {:noreply, assign(socket, counters: counters, stats: calculate_stats(counters))}
end

This approach ensures that all clients see the same state, with changes propagating in milliseconds.

Visual Feedback with Hooks

For smoother user experience, I added animations when counter values change using LiveView Hooks:

let Hooks = {}
Hooks.CounterAnimation = {
  updated() {
    this.el.classList.add('bg-yellow-100')
    setTimeout(() => {
      this.el.classList.remove('bg-yellow-100')
    }, 300)
  }
}

These hooks are connected to the DOM elements in the LiveView template:

<div class={"text-4xl font-bold #{counter_color(counter.value)} transition-colors"}
     phx-hook="CounterAnimation" id={"counter-value-#{counter.id}"}>
  <= counter.value %>
</div>

Deployment with CircleCI and Cloud Run

Docker Configuration

I containerized the application using Docker with a multi-stage build to keep the image slim:

# Build stage
FROM elixir:1.14-alpine AS build

# Runtime stage
FROM alpine:3.17
# Configuration to run the Phoenix application

CircleCI Pipeline

I set up a CircleCI pipeline for continuous integration and deployment:

version: 2.1
jobs:
  build_and_test:
    # Test configuration
  
  deploy:
    # Deployment to Cloud Run
    
workflows:
  version: 2
  build_test_deploy:
    jobs:
      - build_and_test
      - deploy:
          requires:
            - build_and_test
          filters:
            branches:
              only: main

Google Cloud Run

Cloud Run provides a serverless environment that's perfect for this application:

  • Scales automatically based on traffic
  • Only charges for actual usage
  • Handles HTTPS termination

Lessons Learned

What Worked Well

  1. LiveView's Programming Model: The unified model for both server and client updates made development straightforward
  2. PubSub for Real-Time Updates: Phoenix's built-in PubSub system made broadcasting changes effortless
  3. Cloud Run Deployment: The serverless approach simplified operations

Challenges

  1. Initial Setup: Getting the Docker configuration right for Elixir releases took time
  2. Testing WebSockets: Had to learn how to properly test real-time functionality
  3. CI/CD Configuration: Setting up proper environment variables and Google Cloud authentication in CircleCI required careful attention

Code Structure and Organization

I organized the project following Phoenix conventions:

lib/
├── counter_dashboard/
   ├── counters/            # Domain logic
      └── counter.ex       # Schema
   └── counters.ex          # Context
├── counter_dashboard_web/
   ├── live/
      ├── counter_live.ex        # LiveView module
      └── counter_live.html.heex  # Template

This structure keeps the domain logic separated from the web layer, making the code more maintainable.

Conclusion

This project demonstrates how Phoenix LiveView can create rich, interactive experiences without the complexity of a separate frontend framework. The real-time counter dashboard may be simple, but it showcases many of LiveView's core strengths:

  • Real-time updates across clients
  • Server-rendered HTML with client interactivity
  • Minimal JavaScript
  • Full-stack Elixir development
  • Built-in PubSub for broadcasting

The deployment to Google Cloud Run via CircleCI completes the picture, showing how LiveView applications can fit into modern CI/CD and cloud deployment workflows.

For developers interested in real-time web applications, Phoenix LiveView offers a compelling alternative to the traditional SPA + API architecture. It's especially powerful for applications where server state needs to be the source of truth and shared across clients.

Next Steps

This is just the first project in my LiveView learning journey. Stay tuned for my next post, where I'll cover building an interactive Todo application with drag-and-drop reordering and live form validation.

In the meantime, you can check out:


How are you using Phoenix LiveView? Let me know @jumabaraka