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:
llms.txtandllms-full.txt- LLM-friendly documentation, served on the Hologram websiteusage-rules.md- conventions and gotchas shipped with the package, compatible with the usage_rules ecosystemmix holo.gen.agents_mdandmix holo.gen.claude_md- generate anAGENTS.mdorCLAUDE.mdfor your project
The result: assistants stop suggesting LiveView patterns that don't apply and start writing idiomatic Hologram.
Faster Dev Cycles with mix holo
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:
- plug bumped to 1.19.2 to patch a multipart header parsing denial-of-service (CVE-2026-8468).
- decimal bumped to 3.x to patch an unbounded-exponent denial-of-service in
Decimal.new(CVE-2026-32686).
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:
- Main Sponsor: Curiosum - ongoing sponsorship along with business insight and adoption guidance, helping shape Hologram's roadmap based on real-world production needs
- Milestones Sponsor: Erlang Ecosystem Foundation - milestone-based stipend, helping fund key development goals
Thanks also to our GitHub sponsors:
- Innovation Partner: Sheharyar Naseer (@sheharyarn)
- Framework Visionaries: @absowoot, Oban (@oban-bg), Lucas Sifoni (@Lucassifoni), Robert Urbańczyk (@robertu), Moss Piglet (@moss-piglet)
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