Why the Squilla admin is just a shell

The core admin is about 4,000 lines of React. Every feature page lives in its own extension.

I want to put a number on something before I explain it. The entire Squilla core admin SPA is about 4,000 lines of React. That covers the auth flow, the sidebar, the dashboard, and the extension loader. That's the whole thing. Every feature page you see in the admin (media library, email rules, forms, SEO config, sitemap settings) is a separate Vite build owned by an extension and loaded at runtime via import maps from window.__SQUILLA_SHARED__.

That number is the point of the post. Everything below is the reasoning for why it stays small.

What the core admin actually does

If you grep the admin-ui folder you'll find four real responsibilities. It signs users in and holds the session. It paints a sidebar from a menu manifest that extensions contribute to. It renders a dashboard that's mostly a welcome screen and some health widgets. And it has an extension loader that reads each installed extension's manifest, resolves its admin UI bundle URL, and mounts that bundle into a route.

That's it. There is no media UI in core. No email template editor in core. No SEO settings panel in core. If you uninstall every extension, the admin still boots, but the only thing it can do is log you in and tell you the dashboard is quiet.

How the extension UIs get in

Each extension ships a tiny admin-ui/ Vite project that outputs an ES module. The module exports a React component (or a small route table). Shared dependencies like react, react-dom, @squilla/ui, @squilla/api, and @squilla/icons are not bundled into the extension build. Instead, the core shell publishes them on window.__SQUILLA_SHARED__, and an import map shim rewrites the extension's bare imports to read from that global at runtime.

The net effect is that each extension's admin bundle is small (think tens of kilobytes, not megabytes) and every extension uses the exact same React instance, the exact same shadcn primitives, and the exact same API client as core. The chrome looks consistent because everyone is literally importing the same components.

Why this matters

Four things fall out of the shell-only design, and they're the reason I keep defending it in code review.

1. The core admin doesn't accrete features. Every CMS I've worked on has a core admin that grows by a thousand lines a month because someone added a feature and the natural place for its UI was the existing admin. Squilla's core admin can't do that. The UI for a feature lives with the feature, in the extension folder, owned by the extension's author. The shell doesn't have a media tab to extend.

I love this rule because it makes scope creep architecturally inconvenient. If a contributor wants to add an analytics panel, the conversation isn't "where in the admin should this go". It's "this is an extension".

2. Extensions ship their own React UIs and don't fight over the bundle. Two extensions built by two different teams can both ship modern React with hooks, state libraries, and form helpers without colliding. They share React via the import map, so there's only one React on the page, but their feature code is isolated. Nobody is editing the same component file.

3. You can update one extension's admin without redeploying core. An extension's admin bundle is just a file on disk in the extension folder. Replace the file, the loader picks up the new module on the next page load. No core rebuild, no admin SPA redeploy, no Vite cache to bust. The extension hot-deploy pipeline drops new bundles in atomically.

4. Third-party developers don't need to touch core to ship a feature page. This is the one that matters most for the ecosystem. If you want to ship a Squilla extension, you write your gRPC plugin, your admin React, your migrations, and your manifest. You never open the core repo. The same surface a built-in extension uses is the surface a third-party extension uses. There's no internal API.

Compare with the alternatives

WordPress is the obvious foil. Every plugin's admin lives inside WP-Admin's chrome. The plugin authors render PHP pages into WordPress's screens, hook into WordPress's menus, and inherit WordPress's styles. It works, but the result is that every plugin is a tenant in a building owned by core, and the building keeps getting renovated. A WordPress update can break a plugin's admin because the chrome moved.

Strapi went the other way and made everything one big monolith. The admin is a single Next-ish app where plugin code and core code live in the same bundle. Adding a plugin means rebuilding the admin. Updating a plugin means rebuilding the admin. There's no isolation between plugin UIs and core UI, and the bundle grows with every plugin you install.

Squilla sits in between. The shell is core, the feature UIs are extensions, and the boundary between them is an import map and a manifest. It's the boring middle path, and I think it's right.

The cost, because there is one

I won't pretend this is free. Import map setup is fiddly. Getting the shim to resolve @squilla/ui correctly in dev mode, in production, and inside an extension's own Vite preview took longer than I wanted to admit. Shared dependencies need version coordination, which means we publish @squilla/ui with semver discipline and the shell pins a known-good range. If an extension imports a version we don't ship on the global, we either upgrade core or the extension downgrades.

The fix is mostly documentation and tooling. The new extension scaffold pre-configures the import map shim, the type definitions for the shared globals are published, and there's a CI check that flags an extension importing a dependency the shell doesn't expose. Most contributors never hit the rough edges.

The rule

If I had to write the rule on one line: core does the minimum that lets every extension do its own thing. It's a boring rule with a big payoff. The shell stays small, the extensions stay independent, and the contract between them is small enough to actually keep.

← all posts