← writing

VDUS: the dynamic UI system behind Squilla admin

The Squilla admin used to be a React app that fetched JSON and decided what to render. VDUS flips that. The server sends the layout, the client renders it, and the whole thing stays in sync over SSE. Here is what that buys you.

For a long time the Squilla admin was a perfectly ordinary React app. It fetched JSON, it decided what to render, it kept state in its own head, and when something on the server changed it had no idea unless I taught it to poll. That is the default shape of every admin panel I have ever built, and I had stopped noticing it was a choice at all.

VDUS is the moment I stopped doing it that way.

What VDUS actually is

VDUS stands for the Visual, or Server-Driven, UI System. The naming is deliberately fuzzy because both halves are true. It is server-driven in the sense that the server decides what the screen looks like. It is visual in the sense that it ships a description of the visible tree, not a stream of data to be assembled on the client.

The simplest way I can explain it is the inversion. In the old world, the client owned the page. It asked the server for data, then it ran its own logic to decide which components to mount, which tabs to show, which fields were disabled. The server had no opinion on layout. It just returned a blob and trusted the client to know what to do with it.

In VDUS the server owns the page. It sends down a layout tree, which is a small JSON document describing nodes like a panel containing a tabset containing a form containing three fields. The client walks that tree and renders the corresponding React components. The client does not decide what to show. It is told.

The moving parts

There are four of them, and each one earns its place.

The boot manifest is the initial layout tree. When you open an admin page, the server computes the entire layout for that route and hands it over in one document. No waterfalls, no spinner cascade, no first paint that is missing half its content while three more requests resolve. The page boots with everything it needs to render itself fully.

The component registry is the small fixed set of React components the client knows how to render. Panel, tabset, form, field, button, table, banner, and a handful more. The registry is intentionally small. It is not a generic UI toolkit. It is the vocabulary the server is allowed to speak, and every word in it has been picked because it shows up everywhere in admin UIs.

The action handler is what happens when the user clicks a button or submits a form. The button does not have a JavaScript onClick. It has an action descriptor, something like a name and a payload. The client hands that descriptor back to the server, the server runs the action, and the server responds with a new layout tree. The client diffs the tree against what it already has and updates the screen.

The SSE channel is the part that makes the whole thing feel alive. The client holds an open server-sent events stream. Whenever something changes that affects the current page, even if a different user changed it, the server can push a fresh layout fragment down the wire. The client merges it in. No reload, no manual refresh, no polling.

A concrete example

Here is the one I keep using when people ask me what this looks like in practice.

An editor opens the settings page for the SEO extension. The boot manifest comes down with the full form, including current values, validation hints, and the current success banner state, which is empty. The editor edits the meta description, types something too long, and the form complains client side because the field descriptor included a max length. So far this is unremarkable.

The editor saves. The client sends an action descriptor to the server. The server validates again, writes the change, and responds with a new layout tree where the success banner is now populated and the field shows the saved value. The client diffs and updates. There is no separate request to refetch the settings, no separate state machine for the banner. The server just told the client what the page looks like now.

Meanwhile, another editor in another tab is also looking at the same page. The SSE channel pushes the new layout to them too. Their banner appears, their field value updates, and nobody had to write a single line of WebSocket plumbing or cache invalidation logic to make it happen.

Why this works well for a CMS

The reason I built VDUS at all is the extension model. Squilla extensions ship their own admin UIs. In the old approach, that meant every extension had to compile a React micro-frontend, register it with the host, and own a slice of the React tree at runtime. That works, and Squilla still supports it for extensions that genuinely need custom interactions, but it is heavy.

With VDUS, an extension can describe its admin UI as a layout tree generated by its plugin. No micro-frontend, no Vite build, no import map shims, no React at all. The plugin emits layout descriptions, the host renders them, and the extension never touches the client tree directly. It is a smaller surface to ship, a smaller surface to break, and a smaller surface for a plugin author to learn.

The host stays in charge of the rendering. The extension stays in charge of the meaning. That separation is exactly the one I have wanted in every plugin system I have ever worked on.

The honest take

VDUS is not free. The trip back to the server on every action is real. For super-interactive UIs, things like canvas editors, drag heavy layouts, live previews with sub-frame latency, this approach is the wrong tool, and I do not use it there. Those screens are still React micro-frontends with their own state machines, because they have to be.

For everything else, which is the overwhelming majority of an admin panel, VDUS is faster than what it replaced. The boot is faster because there is no second wave of fetches. The mental model is smaller because there is one place layout lives. The bugs are fewer because client and server cannot drift out of sync about what should be on the screen. They cannot drift, because the server is the only one with an opinion.

It is also easier to reason about for plugin-style apps. I can read an extension's plugin code and know exactly what its admin page does, because the layout is right there in the response. I do not have to spelunk through three React files and a Redux slice to figure out which component renders when.

Server-driven UI feels old

I know how this sounds. Server-rendered UI is the thing the industry spent a decade moving away from. We went all-in on rich client apps because the round trip was the enemy. Then we moved some of it back to the server with SSR and RSC because the bundle was the enemy. VDUS is one more step in that direction, and to anyone who built admin tools in the 2000s it is going to feel like deja vu.

It does feel old. It also works. The difference between then and now is the SSE channel and the fact that the layout tree is a structured document the client can diff, not an HTML blob the browser has to re-parse. The trip back to the server is cheap, the update is surgical, and the user does not see a flash.

Server-driven UI feels old. It works new. That is the whole pitch, and after a year of building Squilla on top of it, I am not going back.

Want more like this?

Occasional, opinionated, no listicles.
all writing →