About Squilla
A CMS designed around one premise: an AI agent should be able to scaffold, populate, and operate an entire website without ever opening a shell. We built the kernel small, the contracts public, and every operation reachable through ~75 MCP tools.
Read the standardsWe started with a different question.
Most CMS platforms ask: how do we make this easier for content editors? That's a fair question. It's also a solved one — the major platforms have answered it for fifteen years.
Squilla asks a different question: how do we make this trivially obvious for an AI agent? An LLM can't click around an admin panel, can't infer where a setting lives from documentation written for humans, and shouldn't have to scrape HTML to figure out which blocks exist on a page. So we built every operation as an MCP tool with a typed contract, returned everything as JSON, and made the contracts themselves discoverable.
The outcome turns out to be just as good for humans. The admin shell is small because the API is complete. New extensions snap in because the surfaces are documented. And when something goes wrong, you can read the call that caused it.
By the numbers
If disabling or removing an extension would leave dead code in core, that code belongs in the extension. Core stays a kernel.
from CLAUDE.md, line 24
Three principles, exhaustively applied.
Kernel-only core
Content nodes, auth, rendering, the event bus, and the CoreAPI. That's the whole kernel. Media, email, forms, sitemaps, image optimization — every feature you'd expect a CMS to have lives in an extension that owns its full vertical slice.
MCP is the public contract
The MCP server is the canonical API. The admin SPA uses it. The CLI uses it. AI agents use it. The same surface, the same auth, the same rate limits — no shadow endpoints. core.guide teaches the surface in one call.
Hot, idempotent, observable
Activation is hot. Migrations are idempotent. Renders are side-effect-free. Drop a folder onto disk and the fs watcher picks it up. Deactivate an extension and its plugin subprocess dies, its routes go cold, and core stays clean.
Three views of the same system.
Squilla is a Go application built on Fiber and GORM, using PostgreSQL with JSONB everywhere blocks_data lives. The kernel exposes a single CoreAPI Go interface (35+ methods across 15 domains) which is then adapted three ways: internal direct calls for core code, gRPC via HashiCorp go-plugin for extension binaries, and Tengo via core/* sandboxed modules for theme and extension scripts. Capability checks are enforced uniformly across all three adapters.
Public site rendering uses Go's html/template with custom funcs (filter, event, renderLayoutBlock, image_url, image_srcset). Layout, partial, and block templates each see different scoped data — call core.theme.standards for the contract.
For themes: drop a folder in themes/ with a theme.json manifest, optional scripts/theme.tengo seed, and your blocks/layouts/partials. The theme self-bootstraps on activation — registers node types, taxonomies, terms, settings, menus, and seeds demo content.
For extensions: same shape under extensions/, plus an optional Go gRPC plugin in cmd/plugin/, an optional React micro-frontend in admin-ui/, and SQL migrations in migrations/. The manifest declares capabilities; the kernel enforces them on every CoreAPI call.
Both surfaces have a core.{theme,extension}.standards MCP tool that returns the authoring contract as structured JSON.
One Docker image. Multi-arch. The compose file boots Postgres + Squilla and that's it — every reference extension ships inside the image. SQUILLA_SECRET_KEY protects encrypted settings (AES-256, exactly 32 raw bytes); SESSION_SECRET signs cookies; MONITOR_BEARER_TOKEN guards the stats endpoint.
Operationally: theme/extension activation is hot. Drop-in is hot. Plugin subprocesses are isolated, so a panic in one extension can't take down the kernel. The fs watcher coalesces FS bursts so a cp -r fires exactly one rescan after the burst settles.
Twelve rules we don't break
-
The manifest is the contract.
Every binary, route, capability, block, field type, and admin route is declared in extension.json. If it's not there, the kernel can't see it.
-
Capabilities are minimal.
Declare only what you call. Adding a capability is cheap; explaining data:write on a read-only extension to a reviewer is not.
-
{ "error": code, "message": text } is the error envelope.
Every error response — admin or public — uses this shape. Clients read data.error and data.message.
-
Public routes mount at the path you declare.
Not under /api/, not under /admin/. The path in public_routes[].path is the path users hit.
-
Tables are prefixed by the extension's domain.
forms, form_submissions, media_files. Never collide with core tables.
-
JSONB normalizes before iterating.
Strings come back where objects should. Always Unmarshal declared keys before you touch them.
-
Asset references survive theme switches.
Use extension-asset:<slug>:<key> and theme-asset:<key>. Never hardcode /media/... paths in templates.
-
Per-extension Tailwind builds are mandatory.
The admin shell's @source over extensions/ only helps in dev. Docker stages skip it.
-
Use the design system primitives.
ListPageShell, ListHeader, EmptyState, Chip, StatusPill — reach for them before rolling your own.
-
URL params own filter/sort/view/pagination.
Refresh preserves state. Default values omit the param. replace:true for keystrokes.
-
Production code under 300 lines per file.
500 is the hard limit. Test files exempt. Split early.
-
Don't reach for a slot when an event-with-result will do.
Slots couple admin UIs at build time; events couple at runtime. Pick the looser coupling.
Going deeper
Because monolithic CMSes accumulate features that 30% of users need and 70% have to work around. Squilla flips it: the kernel is what every CMS needs — content storage, rendering, auth. Everything else is an opt-in package. Disable the email extension and the kernel doesn't know it ever existed. Swap the SMTP provider for Resend by toggling two extensions. Replace forms entirely with your own form engine — the kernel doesn't care.
The hard rule: if removing a feature would leave dead code in core, that code belonged in an extension all along. We've used this rule to refactor cache routing, image optimization, and form rendering out of core. Each time, core got smaller and the extension got the focus it deserved.
Three things. One: every operation is a typed MCP call, so an LLM can scaffold a new content type, seed pages, and verify the render in one chain — no shell, no SQL. Two: core.guide teaches the platform in one payload (decision tree + recipes + state snapshot + tool index), so cold-start cost for an agent is constant rather than O(n) discovery calls. Three: the standards are themselves MCP tools — core.theme.standards and core.extension.standards return the authoring rules as structured JSON so an agent can validate its own output before shipping it.
Capability declarations + process isolation. Every extension declares the CoreAPI capabilities it needs in extension.json. Try to call SendEmail without email:send? You get ErrCapabilityDenied, the call short-circuits, no email is sent. The capability set is small enough to audit at a glance — a reviewer sees data:write on a read-only extension and asks why.
Plugins run as separate gRPC subprocesses via HashiCorp go-plugin. A panic in one plugin kills only that subprocess; the kernel and other extensions keep serving. The plugin restarts on next activation. Crash isolation is a property of the architecture, not a feature you have to remember to use.
The kernel and the eight reference extensions ship in the published Docker image. Coolify and bare-Docker deployments are documented. Auth, sessions, RBAC, CSRF, encrypted settings, monitoring tokens, audit logging — all present. We're using it for our own marketing site (this one) and a handful of customer projects.
What it isn't yet: a horizontally-sharded SaaS multi-tenant platform. The single-instance design assumes one Postgres, one app process, one filesystem. If you need that, the architecture supports it but the operations playbook doesn't exist yet.
Want to see it?
The fastest way to grok Squilla is to call core.guide from any MCP client and watch the decision tree come back. Or read the architecture doc — it's the same content, tuned for human eyes.