PhoenixDatastar - A LiveView-like experience for Phoenix using Datastar's SSE + Signals architecture

Sharing my project for review and feedback. Be gentle, my self-taught engineering degree is on spreadsheets and a no-code platform (Bubble.io).

AI disclosure
Mix of AI-assisted and hand-written code. Core architecture and design decisions are mine; AI helped with boilerplate, docs, and some implementation details. Also, I incessantly bothered @mcass19 , but he can’t do miracles. The bugs are 100% artisanal, hand-crafted by me.

What
A Phoenix framework integration that gives Elixir developers a LiveView-like experience using Datastar’s SSE + Signals architecture instead of WebSockets. You write familiar Phoenix callbacks (mount, handle_event, handle_info, render) with HEEx templates, but the transport is Datastar’s SSE protocol under the hood.

What it does today

  • Two view types: stateless (:datastar) and live (:live_datastar — GenServer-backed with SSE push)
  • Signal management (put_signal, update_signal) for client-side reactive state
  • Server-side assigns with patch_elements for granular DOM updates via SSE
  • igniter.install script
  • usage_rules skills
  • handle_info support for PubSub/timers — real-time multi-user updates work
  • event/2 macro for handling Datastar actions with signal data: it creates a @post with the csrf token.
  • execute_script, redirect, console_log
    Working examples included (counter, multiplayer counter via PubSub)

Goals / what’s not there yet

  • Alpha stage — the API is still changing. Not production-ready.
    • That said, I don’t listen to my own advice, so I built a little service with it: https://tiptap.rico.wtf. It has a thriving user base of one (me). No incidents reported so far.
  • Needs alignment with the Tao of Datastar
  • Error handling and edge cases need hardening

Would love feedback on whether the API surface feels right to Datastar users, even if you’re not familiar with Elixir.

Hex: phoenix_datastar | Hex
Repo:

GitHub - ricotrevisan/phoenix_datastar: A LiveView-like experience for Phoenix using Datastar's SSE + Signals architecture. · GitHub

Appreciate any feedback — especially from folks who’ve built SDKs for other backends.

6 Likes

Thanks for sharing! I think it’d be nice if there was a go to Elixir + Datastar combo. This approach feels very similar to LiveView which some may like though imo since it’s so similar my first thought would be maybe I should just use LiveView.

I think there’s an opportunity to radically simplify this combo as a unique alternative framework. I shared some thoughts previously in another thread: Nex - A minimalist web framework for indie hackers and startups - #26 by jam

2 Likes

Thank you! That’s the same thing that Delaney said when I posted on the Datastar Discord.

I would love to hear more about your ideas.

In the meantime, let me brain-fart some of the thoughts I had while building it – maybe you guys can spot where I went wrong and I can learn how to improve it.

  • there were some libraries that handled the SSE part of Datastar. I wanted that frontend-backend glue.
  • I’m rather ignorant on other patterns; Elixir Phoenix was ‘my first’. I tried to mimic that.
  • my main desire is to have some realtime pages, I figured I “just replace websockets part of LiveView”.
  • for realtime, the server needs to keep track of open connections – so GenServer on server.ex + registry.ex – does that job and keeps the sse stream open.
  • to get the stream open, I add a @get('_stream') with the default_html.ex
  • I like Phoenix’s way of keeping the page’s HTML and the actions together in one module. So I made a "gambiarra " macro to @post to the server and then have the server figure out which module it should dispatch the call to.
  • the datastar_session came about because when navigating from one live_datastar page to another, it would break in a weird way

An update.

After all the feedback, I’ve drastically simplified it in Dstar. the key learning: it’s a return to a more DeadView style.

It took me a while to be productive and now it’s much simpler for me to understand:

  • 1 auth path via Plug
  • no need to hand over stuff from conn to assigns
  • no websocket-stutter
  • (most) every page load is an entire page
  • easy to snipe target a HTML mutation - no need to send messages across
  • fully reactive

I’ve been using it on a few small projects to explore where it breaks and it’s been solid so far.

Note to self:

  • I need to figure out how to get Zed / HEEx to work with valid Datastar patterns
  • The Datastar Discord often talks about Web Components: I should learn more about that.

Nice, looks better. Thanks for working on this. The one thing tripping me up is

post "/ds/:module/:event", Dstar.Plugs.Dispatch,
  modules: [MyAppWeb.CounterEvents]

Feels a bit off to list all modules here. I think it’d be nice you could do:

post "/ds/:module/:event", Dstar.Plugs.Dispatch

# in the modules
defmodule MyApp.Something do
  use Dstar # add this
  …
end

Maybe there’s an even simpler approach but that feels a bit more straightforward to me. Thoughts? Have you received any other feedback in the Datastar discord or elsewhere?

Fwiw, I think a Todos MVC example with optimistic UI would be useful to show people how the pieces fit together in addition to the counter example.

1 Like

i’ve created this based on Dstar this is still in very early days and i’m working through somethings:
https://hexdocs.pm/octa_star

But it solved some issues with Dstar for me.
Auto register controllers
Auto start for handle_events
align handle_events with LiveView

I’m also working on OctaStarAsh. which will add form helpers for working with Ash and data-star
although we most likely can combine this into Dstar if you’re up for it @RicoTrevisan

2 Likes

Nice! I like that you have everything in one spot – initializing signals, rendering the template, and handling events. I wonder if it warrants a different name since it’s not a traditional controller at that point. Maybe it doesn’t matter and people will figure it out. Thoughts?

Maybe:

defmodule MyAppWeb.Counter do
  use MyAppWeb, :starview
  ...
end
1 Like

it could be, although it’s just use OctaStar need redo the name to StartView :smiley:
I think yeah let’s make that explicit i’ll update the igniter install to do this

1 Like

Also maybe def show should be def init or similar. show feels off to me.

1 Like

redit the example with showmount and htmlrender so it matches live_view very closely now

1 Like

Looks good. I like the example. Btw, there’s still def show in the Readme.

Regarding the when to use which, it seems to me that Datastar is perfectly capable of handling realtime collaborative systems (they even say it specifically on their homepage). You also don’t necessarily need to always do explicit patches and can rely on fat morph for automatic diffing as they suggest in the Tao of Datastar. :slight_smile: I also think Phoenix.Presence could be used with this. I think the real advantages Liveview has are 1) maturity and 2) absolute minimal data over the wire, which I think could be negligible in a lot of cases with compression. It would be interesting to see a head to head on a more complex, realtime collaborative web app.

1 Like

Yeah i think i need to do a live_beats example with this

1 Like

I’m up for whatever expands the Datastar universe. And if it merges with Ash even more.

Datastar still doesn’t have an official SDK for Elixir. I’m asking this because I’m wondering if we should first create a SDK that we can all build upon. Ideally that would be named Datastar, but I think that is a Gleam package. Pardon my ignorance, but can I use a Gleam package in Elixir?

For the Phoenix-like style, I don’t have the bandwidth to maintain PhoenixDatastar, but it is a good name for what it could do. If anyone wants to take it over, please do.

Thank you for the suggestion. Let me take a look, I can’t remember why I made that decision, but it does look like it could be simplified.

1 Like

Yes you can use gleam in elixir :smiley:
I’m walking through all the code in StarView I basically just took your code. But at the moment making sure it makes sense as i’ve vibe coded to the max.

1 Like

This boy got 1 billion checkboxes going: http://checkboxes.andersmurphy.com

Did you just rename the package from OctaStar to StarView? I feel like I went through the same flurry of activity when I started working on PhoenixDatastar. :smile:

I’ve been reading through the code and I’ve got a few questions. I’m trying to learn from the design choices you made.

On tabId / stream deduplication
I noticed StarView.start_stream/2 uses the same core idea as Dstar: read tabId, key the registry by {scope_key, tabId}, kill the previous process, then start SSE. That makes sense to me.

In the README example you show:

<div data-signals={~s({"tabId": "#{Ecto.UUID.generate()}"})}>

but the module docs show

<div data-signals={~s({"tabId": "${crypto.randomUUID()}"})}
     data-signals-on-load={~s(sessionStorage.setItem('tabId', $tabId))}
     data-signals-on-load={~s($tabId = sessionStorage.getItem('tabId') || $tabId)}
>

Which one will you keep?

On starting persistent streams
In the Dstar docs I recommend this pattern:

data-init="@post('/stream', {retryMaxCount: Infinity})"
data-on:online__window="@post('/stream', {retryMaxCount: Infinity})"

I got this pattern from Anders Murphy’s Billion Checkboxes. You didn’t copy it - was that intentional? Are you expecting users to rely mostly on Datastar’s default retry behavior, or did you leave stream startup as more of an application-level concern?

I’m especially curious about the online__window part. I’ve been documenting it as useful when a laptop wakes up, WiFi drops, etc. Do you think that is unnecessary with Datastar’s retry handling, or just omitted for simplicity?

On HTTPS
I like how you approach it and just do it for the user.
Were you also hitting the max number of connections when using HTTP/1.1?

On Assigns + Signals
Why did you decide to keep both?

3 Likes

Yes :smiley: But I have a fondness for good names :smiley: and StarView really sounds nicer than OctaStar :smiley:

Most of the code is from Dstar i really just did a few helpers on top.

On tabId / stream deduplication
I’m still experimenting, but the week has been very busy with work. I will speak to Delaney if we can skip something there.

On starting persistent streams
I’m yet to fully experiment with that, but i would keep it at application level concern maybe provide a <StarView.page live=“someID”> helper to make things easier but would still mention the online__window thing. But again will have to confirm more with Delaney

On HTTPS
I didn’t hit the limit, but i just thing it’s a good practice, what I would really love to setup is something like "#{:otp_app}.test” url and have a task mix starview.start which would open that page in the default browser.

On Assigns + Signals
First of assigns/3 is just the native Phoenix way of assigning things to the conn, so it doesn’t send any signal to the client. I use this when i need something for the patch_elemen(&function_component) but i don’t want to send that data to the client.
signal does two things:

  1. it uses assigns(conn, key, value)
  2. it puts the key in the conn.private map so that i can then call StarView.flush_signals which will automatically enum over the private keys and set the patch_signals
    The reason is simply nicer Developer experience since i want the signals to always be a part of the conn.assigns.
    I suppose i could also avoid the flush_signals and just do assing and patch_signals in one go. I think i will simplify this.

Overall
I’m still in heavy experimentation phase, but the main goal of StarView is to port all the examples from the DataStar page and show how to do them with the Phoenix Framework. And in the process of it create the best Developer experience possible.

P.S.
I believe there might be room for automatic change tracking from the Phoenix LiveView core, but relying on signals or the Stream deduplication, to just have the full render and extract only the changed things to create automatic patch_signals/patch_elements but this would be further down the line, it’s also much more costly on the server.

StarView ftw

Nice! Love this goal.

This is an interesting idea. I think there could be nice abstraction here. It’d be nice to have things a bit more declarative and have things “just work” without the patch_* everywhere. :slight_smile:

1 Like

Deprecated: please see: Dstar - reactive Phoenix pages over plain HTTP and SSE, no websockets