Welcome to KeenSpace, Ada
A Teams-style workspace demo for keen_phoenix_svelte
— a library that auto-mounts compiled Svelte 5
apps into Phoenix
(LiveView and
plain pages) as
self-contained islands.
Each area is an autonomous Svelte app: configured, handed a connection, then fully
self-owning. No ~V
sigil, no server-rendered slots, no SSR — the opposite of interleaving Elixir and Svelte.
Three areas, three transports
Every island is a separate compiled bundle, mounted into a plain <div>.
What differs is how each one talks to the server.
Chat
channel + Presence
Team rooms with live messages and an online-presence list, over a Phoenix channel. Click an avatar and the host LiveView loads that person's profile beside the island.
OpenVideos
api + live
A catalogue loaded over REST with an in-page Plyr player. "Save" pushes over the LiveView socket — or falls back to api.post on a plain page.
OpenCalendar
context.tokens + fetch + channel
Today's agenda from a simulated Microsoft Graph via a bearer token. "Join online" opens a per-meeting chat over a channel — one island, two transports.
OpenThe app boundary
Every island's entry receives one object — that's the entire coupling to Phoenix:
(target, { props, context, live, api, channel, bus, el }) => handle
props
Small per-island config, rendered by <.svelte props={…}>.
context
Page-wide user, CSRF, tokens, api_base and socket — emitted once per page.
live
The LiveView bridge: pushEvent / handleEvent / upload. null on plain pages.
api
REST helper; attaches x-csrf-token + the session cookie.
channel
Promise-based Phoenix channel factory with auto cid correlation.
bus
Page-wide client-side event bus for island-to-island messaging — no server.
Any framework — same boundary
The three areas above are Svelte, but the mount contract is
framework-neutral. Below are three more islands — Lit, React and vanilla JS —
each mounted through the same <.app>
component. There are no per-framework variants; the optional framework
attribute is just an informational data-framework
tag. Click them — they all reach the activity bus.
Each passes its own <:placeholder>
shaped like the control it's loading — a pill for Kudos, four buttons for
reactions, a bar for the ticker — instead of the server-wide skeleton. Throttle
your network (DevTools → Slow 3G) and reload to watch them resolve.
<.app framework="lit">
A Lit web component (shadow-DOM styles).
<.app framework="react">
React 18 + hooks, JSX built by esbuild.
<.app framework="js">
Plain JavaScript — no framework at all.
Islands talking to each other — the event bus
live, api
and channel
all reach the server. The bus
is different: a page-wide, client-side
event bus so independent islands can coordinate without the server and without knowing about each other. It behaves
identically with or without LiveView.
activity island
Mounted once in the shell. Knows nothing about the others — it just
bus.on("activity", …)
and shows a toast.
video-catalogue
On save →
bus.emit("activity", {title: "Saved…"})
calendar
On "Join online" →
bus.emit("activity", {title: "Joined meeting"})
Try it: save a video or join a meeting and watch the toast fire in the corner — three separate bundles, coordinating over the bus.
An island loaded from elsewhere
Islands don't have to live under /apps. The one below is a
framework-free bundle registered in config; the server emits a name → url
manifest and the client imports it from there. A config toggle picks the mode of operation:
-
:direct— the browser imports the CDN URL itself (needs CORS + a permissive CSP). -
:proxy— the browser imports a same-origin path and Phoenix fetches the bundle upstream (KeenPhoenixSvelte.Apps.Proxy) — no CORS, CSP'self', the corporate-friendly mode. Active here in dev.
Works without LiveView, too
The very same island runs on a plain, controller-rendered page via
mountStatic()
with live: null
— proof the apps don't depend on LiveView. See the plain calendar page,
where the agenda and meeting chat work unchanged.