Hologram v0.9: Realtime and More

v0.9 brings a realtime layer to Hologram - your server can now push updates to connected clients, in pure Elixir, with no JavaScript and no client-side polling.

Why Realtime Matters

Modern web apps are live. A chat message appears the instant it's sent, a notification pops up when a background job finishes, a collaborator's edit shows up without a refresh. Until now, building any of that in Hologram meant reaching outside the framework.

v0.9 closes that gap. When something happens on the server, you broadcast an action, and Hologram runs the matching action handler on every subscribed client - the same action/3 handler you already write for user events, just triggered from the server instead of a click. This was the most complex feature Hologram has shipped to date, and the one that took the most work: the realtime PR (#806) landed 414 commits and over 12,000 lines across 108 files.

Here's a complete chat room - init/3 subscribes the page to a room, a command broadcasts each new message, and the action handler runs on every subscriber, including the sender:

defmodule MyApp.RoomPage do
  use Hologram.Page

  route "/rooms/:id"

  layout MyApp.Layout

  def init(%{id: room_id}, component, server) do
    component = put_state(component, room_id: room_id, messages: Chat.recent_messages(room_id))
    server = put_subscription(server, {:room, room_id})
    {component, server}
  end

  def command(:send_message, %{room_id: room_id, text: text}, server) do
    message = Chat.create_message(room_id, text)
    put_broadcast(server, {:room, room_id}, :add_message, message: message)
  end

  def action(:add_message, %{message: message}, component) do
    put_state(component, messages: [message | component.state.messages])
  end
end

The publisher names only the channel and the action, never a component - which is what lets one broadcast update many clients at once. Channels are structured, typed values (:notifications, {:room, 42}, {:doc, "abc-123", "v2"}), not raw topic strings, so a malformed channel is rejected instead of routing nowhere. Alongside the application channels you define, three built-in identity channels - {:instance, id}, {:session, id}, and {:user, id} - let you address a single tab, a session, or every device a user is signed in on. Those ids come from three new server struct fields - instance_id, session_id, and user_id - joined by subscriptions and broadcasts for inspecting what you've queued.

The handler calls above (put_subscription, put_broadcast) also have Hologram.Realtime counterparts for code that runs outside a handler - a background job, a worker, or existing Phoenix code. So a finished job can toast every one of a user's open tabs:

Hologram.Realtime.broadcast_action({:user, user_id}, :show_toast, text: "Upload complete")

The full guide - subscription lifecycle, authorization, and delivery semantics - lives in the Realtime documentation.

The with Special Form

Hologram now supports Elixir's with special form on the client. Thanks to Robert Prehn (@prehnRA), who opened the issue (#690) and implemented the bulk of it.

with {:ok, user} <- fetch_user(id),
     {:ok, account} <- fetch_account(user) do
  put_state(component, account: account)
else
  {:error, reason} -> put_state(component, error: reason)
end

AI Assistant Support

Coding assistants are everywhere now, but Hologram is unusual: it's Elixir, yet it runs in the browser, and its conventions don't match LiveView's. Left to guess, assistants tend to suggest patterns that don't apply. v0.9 gives them the guidance they need (#779).

There are now machine-readable docs and usage rules so tools like Claude Code, Cursor, and others understand how Hologram actually works:

The result: assistants stop suggesting LiveView patterns that don't apply and start writing idiomatic Hologram.

Faster Dev Cycles with mix holo

⚠️ Heads up: behavior change

After upgrading, Hologram's compiler and runtime no longer start automatically in dev and test - this changes how you run your app in development.

The reason: Hologram's compiler adds startup time, which hurt the tight edit-test loop of TDD (#254, reported by @frankdugan3). v0.9 skips it unless you opt in (#767).

When you want the full app with client-side compilation, start it with the new task:

$ mix holo

Or set HOLOGRAM_START=1 to opt in for a given command. Your test suite and iterative development get markedly faster, because you're not paying the compiler's cost on every run unless you ask for it.

Quality-of-Life Improvements

Optional action/3, command/3, and init/2 callbacks. Thanks to Andrew Haust (@sodapopcan), pages and components now declare action/3, command/3, and init/2 as optional callbacks (#764 / #765). Annotate them with @impl true - or the explicit @impl Page / @impl Component - and the compiler will catch typos in their names and arities. One thing to watch when upgrading: Elixir enforces @impl consistency per module, so if you already annotate template/0 or init/3 with @impl true, you'll now see compiler warnings until you add @impl true to your action/3 and command/3 clauses too.

put_action/3 and put_command/3 accept maps. Params can now be passed as a map, not just a keyword list. Since handlers receive their params as maps already, you can forward a params map straight back into put_action/put_command without converting it to a keyword list first.

Under the Hood

Lower memory usage in development. Justin Wood (@ankhers) fixed excess memory usage during development (#440, reported by @absowoot) by stopping the CallGraph processes after compilation finishes (#749). Long-running dev sessions no longer hold onto memory they don't need.

Node.js resolution fix. Michael Ward (@mward-sudo) fixed a case where the Hologram compiler couldn't find Node.js when a git dependency carried its own stray .tool-versions file (#741, reported by @Blatts12 / #760).

:erlang.binary_to_term/1 ported to JavaScript. Vinicius Pereira (@0bvim) ported :erlang.binary_to_term/1 to the client (#457 / #458) - a substantial port of Erlang's External Term Format decoder, another step in rebuilding the Erlang runtime in the browser.

Security

We patched two dependencies carrying security advisories:

If you depend on these transitively through Hologram, upgrading to v0.9 picks up the fixes.

Sponsors

I'd like to thank our sponsors whose support makes sustained development possible:

Thanks also to our GitHub sponsors:

And to all other GitHub sponsors - thank you. Every contribution, no matter the size, helps keep Hologram moving forward.

If you'd like to support Hologram's development, consider sponsoring the project.

Stay in the Loop

Subscribe to the Hologram newsletter for monthly updates on new releases, features, and community news. You can also join the community to connect with other Hologram developers.

What's Next

Realtime was a big, outward-facing feature. The next release turns inward, deepening the foundation - rounding out Hologram's client-side Elixir runtime and polishing the framework, so more of Elixir, and more of your app, runs in the browser out of the box. Every release brings Hologram closer to the goal it was built for: pure Elixir, end to end.

- Bart

Sponsored by