# How plugins work

> Pull-only acquisition, signed artifacts, verification, and the subprocess runtime.

_Updated: 2026-06-08_

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](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:

```text
catalog  →  grant  →  download  →  verify
```

1. **catalog** — fetch the plugin's entry: its current version, minimum core
   version, content hash (sha256), signature, and the vendor's public key.
2. **grant** — present your license; the vendor returns a short-lived download
   token for the version you're entitled to.
3. **download** — fetch the artifact bytes using that token.
4. **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_TOKEN` credentials,
- 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_core` is newer than your
  core, the core refuses to register it and tells you to update. See
  [Updating](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](licensing-and-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.
