How plugins work
Pull-only acquisition, signed artifacts, verification, and the subprocess runtime.
Updated
Plugins are how Railbase becomes a product. This page explains the hosted distribution model: how a signed plugin gets from the marketplace onto your server at runtime, and how it runs once it's there.
Note
This is the runtime/distribution path — the built-in marketplace, on by default, pulling from railbase.app. To add a plugin to a build you compile yourself instead, register the plugin's Go module at build time; see Installing plugins.
Acquisition is pull-only
In the hosted model, a plugin enters your instance one way: the core pulls it from the marketplace. There is no "install from file", no upload field, and no way to point a marketplace-managed slot at a binary on disk. (Build-time registration is a separate, compile-time path — it doesn't go through the manager at all.)
When you install a plugin, the core runs a four-step exchange with railbase.app:
catalog → grant → download → verify
- catalog — fetch the plugin's entry: its current version, minimum core version, content hash (sha256), signature, and the vendor's public key.
- grant — present your license; the vendor returns a short-lived download token for the version you're entitled to.
- download — fetch the artifact bytes using that token.
- verify — refuse to run it unless all three hold:
- the sha256 matches the catalog,
- the catalog's public key equals the key your instance has pinned for the vendor,
- the Ed25519 signature validates against that pinned key.
Important
The pinned public key is the trust anchor. An artifact that isn't signed by the key you trust never executes — even if a network path is compromised. This is the whole reason sideloading is disabled: every running plugin is one the core cryptographically verified.
How a plugin runs
A verified artifact is launched as a managed subprocess, not loaded into the core's address space. The plugin manager:
- starts the process with a private port and short-lived
ADMIN_TOKEN/DATA_TOKENcredentials, - waits for it to register over loopback, and
- supervises it: a periodic health probe restarts a crashed or hung plugin
(with a restart budget), and traffic to
/papi/<slug>/…is reverse-proxied to the live process.
Running out of process means a faulty plugin degrades to that plugin being unavailable — it cannot crash the core or corrupt another plugin's data.
The register handshake
On startup a plugin announces itself to the core. The handshake carries:
| Field | Meaning |
|---|---|
slug |
The plugin's identifier |
url |
Where the core proxies its API |
colls |
The collections (tables) it owns |
provides / consumes |
Capabilities it offers to / needs from other plugins |
min_core |
The minimum core version it requires |
roles |
The RBAC roles + permissions it declares (and which are billable) |
Two consequences worth knowing:
- Compatibility is enforced. If a plugin's
min_coreis newer than your core, the core refuses to register it and tells you to update. See Updating. - The core doesn't hardcode plugin roles. A plugin carries its roles and teaches them to the core at install time, which is what makes per-seat billing work without a core rebuild. See Licensing & seats.
Why not compile plugins in, or load .so files?
Out-of-process subprocesses avoid the two classic plugin traps: native
.so/plugin loading is platform-locked and ABI-fragile across Go versions, and a
single monolithic binary can't isolate faults. Signed subprocesses give you
cross-platform artifacts, crash isolation, and runtime install/update/remove —
without trusting unverified code.
Note
There is a developer-only path to launch a local, unsigned plugin binary for building plugins. It is admin-gated and separate from the marketplace; it has no role in normal operation and is not how you obtain plugins.