← All writing

C at the Door

The Plugin Boundary That Lets One Runtime Reach From the Server to the Edge

TL;DR: Most backend runtimes are monoliths with a plugin slot. Kalos is the inverse: a C++20 microkernel that is only a plugin system, with the application server assembled entirely from plugins behind a versioned binary boundary. The boundary is the interesting part: C at the door, disciplined C++ inside, with a compatibility handshake checked before any C++ crosses. That’s the same architectural bet PHP’s extension model made, and it’s why the model keeps winning: it lets an ecosystem of independently-built transports, stores, and scripting layers target one host. The payoff isn’t architectural purity. It’s reach — the same runtime that speaks REST to a browser is built to speak to a radio on a gateway, and to keep going down the size curve from there.


The boring win nobody markets

Kalos’s kernel does seven things (configuration, plugin loading, a message bus, the plugin lifecycle, logging, a plugin registry, a local management socket) and refuses to do an eighth. There is no HTTP in it, no SQL, no auth, not even file handling. Every capability the runtime has is a plugin: the REST transport, the SQLite and PostgreSQL stores, the authentication handler, the Lua scripting layer, and (this is the part that surprises people) the ADL engine itself. The thing that parses your YAML and enforces your permissions is a plugin loaded next to the others, not a privileged core they attach to.

Stated as governance, this is the pitch the rest of our writing makes: no privileged core means no side door around the pipeline that enforces your rules. True, and covered elsewhere. This essay is about the other consequence, the one that matters to anyone who has shipped software onto hardware: a kernel with nothing platform-specific inside it is a kernel that goes places.

The PHP lesson, taken seriously

Here is an unfashionable thing to admire: PHP’s extension model.

Set aside opinions about the language. The extension architecture is one of the most successful plugin systems ever fielded. A stable C ABI at the boundary meant that ext/curl, ext/gd, the database drivers, and a thousand others could be built independently, by different people, on different schedules, and loaded into the same host without recompiling the world. Every protocol and datastore on earth ended up reachable from PHP not because the core team wrote them, but because the boundary was stable enough that an ecosystem could. The core stayed small; the reach became enormous. That is the whole trick, and it is a good trick.

Kalos makes the same bet with one deliberate upgrade: a versioned boundary with an enforced load-time handshake. Each plugin exposes an extern "C" entry point returning a plain-old-data struct with an api_version, and the kernel checks that version before it constructs a single C++ object or calls a single method across the boundary. Mismatch is refused at load time with a clean error, not discovered three requests later as a corrupted vtable. C is the language whose ABI is stable across compilers, so C is what guards the door; past the door, where kernel and plugin have agreed they’re compatible, the contract relaxes into disciplined C++: pure-virtual interfaces, values returned rather than exceptions thrown, because exceptions have no business crossing between separately compiled shared objects. C at the door, disciplined C++ inside. PHP proved the model at civilization scale; Kalos tightens the handshake and points it at a different target.

Three proofs the boundary is real

Architecture claims are cheap. Here are three places the plugin boundary is load-bearing today, not in a diagram.

Authentication is a plugin you can replace. The runtime ships authd for identity and auth, and it is not welded in: it is a handler plugin behind ICore, wired in by configuration like any other. Its entry point is a clean, honestly-commented example of the boundary in practice. Swap it for an OAuth-centric handler, an enterprise SSO bridge, a mesh of your own. The kernel does not change, and neither does your ADL. The application’s definition is indifferent to which auth implementation is mounted beneath it. That is modularity you can test by deleting a line of config, not modularity by assertion.

Scripting is a plugin, so Lua is a choice, not a commitment. The in-request extension layer is Lua today, embedded via sol2: sandboxed, instruction-limited, deliberately constrained. But “Scripting” is an archetype, not a hardcoded dependency: the layer sits behind the same boundary as everything else. A Python or JavaScript scripting plugin is the same archetype with a different engine inside. Nothing about the kernel presumes Lua; the kernel presumes only the boundary. What ships is Lua because a small sandboxed language is the right default for logic that runs inside a request. But the architecture is built so that “which scripting language” is a plugin decision, not a rewrite.

Stores are plugins, so the data layer is a mount point. SQLite and PostgreSQL are two implementations of one store interface. The ADL engine does not know which is mounted; it speaks to the store archetype. That’s why the same domain file runs on an embedded SQLite database on a small device and a PostgreSQL cluster on a server. The store is chosen by configuration, not compiled into the application.

Three different subsystems (auth, scripting, storage), each swappable without touching the kernel or the ADL. That is what “modular” has to mean before it’s worth saying.

Reach: from the server to the field

Now the part the architecture was quietly built for.

A transport is just a plugin: the thing that decides how a request arrives and a response leaves. REST over HTTP is the transport shipping today, and to the kernel it is nothing special: a plugin that receives bytes on one side and speaks ICore on the other. Which raises the obvious question for anyone who works with connected devices: if a transport is just a plugin that adapts an external protocol to the bus, the protocol on the outside does not have to be HTTP.

It doesn’t. BLE, Zigbee, and Wi-Fi transports are in development, targeted for Q4: the same Transport archetype REST already occupies, adapting radio and link-layer protocols to the same message bus the rest of the runtime already speaks. When they land, the consequence is one sentence no other declarative-backend runtime can say: the same ADL file that runs your audited back-office system also runs on the gateway, speaking to the devices in the field. Not a separate edge product with a separate model — the same runtime, the same domain definition, a different transport plugin mounted underneath. Client, server, IoT hub, and edge gateway stop being four codebases and become one runtime with different plugins loaded.

This is not a pivot away from the governance story; it is the governance story arriving somewhere new. The reason you would want one runtime spanning the datacenter and the gateway is the same reason you want the declaration to be the application in the first place: one definition to audit, one set of rules enforced the same way everywhere, no reimplementation drift between the server that keeps the records and the hub that talks to the hardware.

How far down does it go?

A monolith’s floor is set by everything it drags along: its HTTP server, its ORM, its assumptions about the host. A microkernel’s floor is set by one number: how small a target can still host the plugin boundary. Strip the runtime to the kernel plus only the plugins a given job needs (a transport, a store, the engine), and the footprint is a function of that boundary, not of a pile of features you aren’t using.

We know where the top of that range is: servers, workstations, ARM single-board computers, all comfortable. Where the bottom is (how small a device can still host a kernel whose only job is to orchestrate plugins) is a more interesting question than it has any right to be, and we are not finished answering it. An engineer who knows what lives at the small end of the hardware curve can probably guess why we find the question worth asking. We’ll leave the arithmetic to them.

The honest limits

Three, because the boundary has costs and a systems reader will find them anyway.

The C++ side of the boundary is a real constraint. STL and JSON cross it, so a plugin must match the platform’s C++ standard library and build configuration. The load-time handshake catches mismatches cleanly instead of letting them corrupt silently. But “catches it cleanly” is not “makes it disappear.” This is a tighter coupling than PHP’s pure-C extensions carry, traded for the ergonomics of writing plugins in modern C++ rather than hand-rolled C.

Load is append-only today. Action registration cannot be replaced or unregistered, so domains cannot be unloaded without a restart and Lua hot-reload can leave a stale handler behind. For a long-running server this is a minor operational note; for a device you reconfigure in the field it is a real constraint, and it is on the list to address rather than something to gloss.

Predictable is not the same as real-time. C++ with no garbage collector gives low, consistent latency and no GC pauses — which is most of what people actually want when they say “fast enough for a hub.” It is not a hard-real-time guarantee with bounded deadlines, and the synchronous bus serializes dispatch. If your problem needs an RTOS’s timing guarantees, this is a runtime that runs near that layer, not one that replaces it.

Closer

The microkernel isn’t built small for elegance; it’s built small so the same runtime that survives an audit in the datacenter can survive contact with the field — one definition, one set of rules, wherever the plugin boundary can reach.