OndrejValenta

OndrejValenta

Keen_phoenix_svelte - Our way to integrate Svelte/React/Lit/Plain JS apps as isolated islands in Plain/Live pages

Greetings and salutations!

We’ve been building keen_phoenix_svelte at KeenMate (now v1.0.0-rc.9), and we’d like your feedback, although we know the library is awesome already :wink:. It auto-mounts compiled Svelte 5 apps (or what have you, React, Lit, even plain JS apps) into Phoenix — on both LiveView and plain controller-rendered pages — as self-contained islands.

Live demo & docs: keen-phoenix-svelte.keenmate.dev.

You write this in a template:

<.app name="dashboard" id={"dash-#{@id}"} props={%{org_id: @id}} />

…and the compiled app in assets/apps/dashboard/ is mounted into that element, kept in sync with server state, handed a bridge to the server, and torn down on live navigation — no manual <script>/<link> wiring.

Motivation

In our corporate development, we kept hitting the same shape of problem: a real Svelte app — a dashboard, an org-structure browser with charts, a data grid — that needs to live inside an existing Phoenix app. Not a sprinkle of reactivity on server-rendered markup, but a substantial front-end app with its own state and lifecycle that just needs a clean line back to the server.

People forget that this includes apps that don’t necessarily communicate with the server directly, they, for example, just display documents stored in Oracle HCM, or they communicate with SAP or any other internal service.

We also didn’t want Node in the production render path, didn’t want to reason about SSR hydration mismatches, and we wanted the same component to work on a LiveView route and a plain controller page.
So the model is: the page places an island, hands it config + a connection, then gets out of the way.

How it differs from live_svelte

They sit at opposite ends of the Phoenix+Svelte space, and both are legitimate:

  • live_svelte interleaves — you render Svelte from Elixir (~V sigil), it does SSR through a Node runtime, props flow through LiveView’s diff on every update. Great for reactive sprinkles.
  • keen_phoenix_svelte isolates — you mount compiled, self-owning apps and talk to them through a narrow boundary. No ~V sigil, no server-rendered slots, no SSR / no Node at render time, no hydration-mismatch class of bugs, and plain (non-LiveView) pages are first-class.

Reach for live_svelte when you want Svelte woven into LiveView pages with SSR. Reach for this when the Svelte side is a substantial app that should own its own world.

How it works

Two cooperating halves:

  • Elixir<.app> renders a hook-bound <div> (phx-update="ignore", data-app, JSON data-props); <KeenPhoenixSvelte.runtime> emits a once-per-page context.
  • JS — a KeenApp LiveView hook mounts the island and lazily import()s the bundle; a mountStatic() scan mounts islands on plain pages.

Each island gets a small boundary: props (config, not payload), context (user/tokens/csrf/socket), live (LiveView bridge, or null on plain pages), api (REST helper with CSRF + session), channel (Phoenix channel factory), and bus (client-side island-to-island pub/sub). The canonical pattern is if (live) { live.pushEvent(...) } else { api.post(...) } — the same component runs unchanged with or without a LiveView behind it.

Neither half is Svelte-specific: the mount contract is just (target, opts) => { setProps, destroy }, so the demo mounts Svelte, React, Lit and vanilla-JS islands through the one <.app> component.

Performance work

  • Only what’s on the page loads — bundles are lazily import()ed per island.
  • Automatic page-scoped module preloading<.runtime> emits <link rel="modulepreload"> for exactly the islands a page mounts, so they download in parallel with the HTML instead of waiting for the socket + hook.
  • Prop-change diffingupdated() skips re-renders when data-props is unchanged.
  • Eager mounting (<.app eager>) — paint an island before the socket connects, then upgrade it with live on connect.
  • Placeholder/skeleton cleared the instant the island mounts — no flash of empty container.

Loading islands from elsewhere — proxying & guarding

If you ever had to deal with security and networking issues, and generally with inability to move forward because of CSP, CORS and similar, you’ll appreciate this feature.

You can register an island whose bundle lives on a CDN or another deploy, in :direct mode (browser imports the CDN URL) or :proxy mode (browser imports a same-origin path and Phoenix fetches upstream — no CORS, script-src 'self', the corporate-friendly mode).

The proxy is what we’ve hardened most:

  • Revalidating cache — bundles held in ETS (concurrent reads, refc-binary bodies), stale entries do a conditional GET, concurrent requests for the same URL collapse into one fetch (no stampede), upstream errors serve last-good fail-open. Correct even for unversioned upstreams, with an end-to-end 304 chain browser → Phoenix → origin.
  • Whole multi-file bundlesbase: (remote dir) / dir: (local, newest content-hashed entry wins) re-serve JS + CSS + assets same-origin from one registration.
  • Security — TLS verified against the system trust store (no MITM on the Phoenix→origin leg), no upstream redirects (SSRF guard), X-Content-Type-Options: nosniff, local sub-paths asserted inside their directory.
  • Bounded fan-out — concurrent fetches capped (excess sheds 503 + Retry-After), definitive misses negative-cached, so a flood of guaranteed-miss sub-paths can’t drive unbounded outbound requests.
  • manifest: allowlist — register the exact files an app ships (inline list, Vite manifest.json, or our keenManifest() plugin), and any other sub-path is a 404 decided before any upstream fetch.

Install

{:keen_phoenix_svelte, "~> 1.0"}   # pin the exact rc for now — SemVer doesn't match pre-releases

Plus the npm package @keenmate/phoenix_svelte (released in lockstep at the same version) and a Svelte/Vite toolchain.

It’s still 1.0.0-rc, so feedback — especially on the boundary API and the proxy — is very welcome. The example/ app is a full “Teams-style workspace” reference (chat over a channel + Presence, a video catalogue over REST, a calendar, plus Lit/React/vanilla islands). Happy to answer questions here.

Where Next?

Popular in Discussions Top

jesse
Hi everyone, I hesitated to post this here because I don’t want you to think I’m spamming, but I’ve been working on a Platform-as-a-Serv...
New
restack_oslo
Hello, Please pardon me for any faux paux. I am 46 and this is my first time on a forum of any kind. I wanted to to get answers from tho...
New
rms.mrcs
A couple of days ago I was discussing with a friend about different approaches to write microservices. He said that if he was going to w...
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
PragTob
Hello everyone, I know we had quite some threads (read through lots of them) about background job processing but it remains a hotly deba...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New

Other popular topics Top

electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 44265 214
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

We're in Beta

About us Mission Statement