Part #4 of our series on building web console for Elixir & Phoenix applications with Phoenix LiveView.

In this series:

  1. Part 1: Project set up and simple code executor
  2. Part 2: Making imports and macros work
  3. Part 3: Managing multiple supervised sessions
  4. Part 4: Making it real-time

Video version

Watch the video version below or or directly on YouTube for a complete step-by-step walkthrough.

Figure out the PubSub server

In order to make our web console real-time, we need to send messages between sessions and LiveViews. The usual way to make it happen in a Phoenix environment is to use Phoenix.PubSub. Our first problem, however, appears to be figuring out what is the pub-sub server the library is going to use.

Since Backdoor is a library, and is going to be mounted in external applications’ routers, it won’t start its own Endpoint, nor start its own instance of Phoenix.PubSub. We need to figure it out, and luckily we have a way to do so. Ideally I also want this as a configuration option, so that users of the library can configure it to something else than the given endpoint uses - for the sake of security, performance or other choice of developer.

We can find out the PubSub instance given endpoint uses when the Backdoor.BackdoorLive mounts:

# lib/backdoor/live/backdoor_live.ex


def mount(_params, _session, socket) do
  Phoenix.PubSub.subscribe(find_pubsub_server(socket.endpoint), @topic_name)


defp find_pubsub_server(endpoint) do
  case Application.get_env(:backdoor, :pubsub_server) do
    nil ->
      server = endpoint.config(:pubsub_server) || endpoint.__pubsub_server__()
      Application.put_env(:backdoor, :pubsub_server, server)

    server ->

The configuration option is then stored in the application environment, but it can be overwritten by the end user setting it in their config/config.ex or similar.

The place where we emit the events is Backdoor.Session.CodeRunner, and it also needs to find out the PubSub instance, but it’s easier since we can reliably rely on application environment variable being present at this moment:

# lib/backdoor/session/log.ex


@topic_name "backdoor_events"

defp log(session_id, value) do
  Phoenix.PubSub.broadcast(pubsub_server(), @topic_name, {:put_log, session_id, value})
  Backdoor.Session.Log.put_log(via_tuple(Backdoor.Session.Log, session_id), value)

defp pubsub_server() do
  Application.get_env(:backdoor, :pubsub_server)

This code makes our code runner emit events, whenever a new log entry is being added. We use similar technique in Backdoor.Session itself, but won’t describe it as it is fairly repetitive, and just link for the reader to have a peek at if interested.

Making LiveView reacting to published events

We already subscribe to broadcasted messages in the mount/2 function of our LiveView, but we need to handle the incoming messages. This is done by introducing appropriate handle_info/2 callbacks to catch published broadcasts.

For example, to handle log messages we need to alter our execute command handler, and instead of handling return value in this function - we add a handle_info/2 callback to catch this event asynchronously:

@impl true
def handle_info({:put_log, session_id, log}, %{assigns: %{current_session_id: sid}} = socket)
    when session_id == sid do
   |> assign(logs: socket.assigns.logs ++ [log])}

def handle_info({:put_log, _session_id, _log}, socket) do
  {:noreply, socket}

@impl true
def handle_event("execute", %{"command" => %{"text" => command}}, socket) do
  Backdoor.Session.execute(socket.assigns.current_session_id, command)

  {:noreply, socket |> assign(:input_value, "")}

There are a few more events that need to be handled in the LiveView, but they largely follow the same pattern: whenever an event happens that all LiveViews need to be reacting to, we publish such events on the PubSub topic and make the LiveViews capture it.


Watch the screencast directly on YouTube

You can find this and future videos on our YouTube channel.

The full code from this episode can be found on GitHub

Post by Hubert Łępicki

Hubert is partner at AmberBit. Rails, Elixir and functional programming are his areas of expertise.