Railbase
GPTClaude

How plugins work

Pull-only acquisition, signed artifacts, verification, and the data-resident goja runtime.

Updated

Video guide —watch on YouTube ↗

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 only customer distribution path — the built-in marketplace, on by default, pulling from railbase.app. Internal dev/embedder build workflows are engineering details, not a way customers receive plugins; 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.

When you install a plugin, the core runs a four-step exchange with railbase.app:

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 not a program the core launches — it's an encrypted JS + schema bundle that runs inside the core's existing goja runtime. There is no subprocess, no private port, no separate address space. The bundle is a row in the core's _plugins Vault collection holding its AES-256-GCM-encrypted code and collection schema; the license gate decrypts it only at the point of execution, so an unpaid, expired, or revoked plugin's code is never decrypted and has zero behavior.

Once decrypted and loaded into the runtime, a plugin reaches the platform only through mediated host bindings — never the core's internals:

  • verbs via $app.routerAdd, mounted in-process and served at /api/<slug>/*;
  • data via $app.dao — the platform's own Vault DB, tenant-scoped; a plugin has no separate store of its own;
  • events via $app.emit(topic, payload) (publish a domain topic) and $app.onEvent(topic, fn) (consume); $app.realtime().publish is the narrower binding for record-shaped CRUD events;
  • background work via $jobs;
  • identity via e.auth — the trusted caller from the request context, never a client header;
  • plan / quota via $app.license.

The verb surface is deployment-invariant: a marketplace-installed plugin serves at /api/<slug>/*, so installing through the marketplace never changes the URLs a frontend calls. All integrations target /api/<slug>/*.

Running in the core's runtime is safe because the bindings are the only surface a plugin can touch: it can't reach *App internals, another plugin's data, or the filesystem outside what the host mediates.

One install delivers the whole plugin

A single marketplace install delivers the plugin including its UI. The end-user frontend ships inside the bundle as bundle/manifest.json (manifest.ui.pages: pages + widgets bound to the plugin's verbs and collections), and the core's site shell mounts it at /<slug> on install — no rebuild, no separate step. Legacy widgets.json files may exist during migration, but the install-time contract is the signed bundle manifest. (The old railbase plugin setup codegen wizard, which wrote raw TSX into a site's web/ and needed a rebuild, was removed; it never fit the data-resident model.)

What the bundle declares

The signed bundle manifest and catalog entry declare, at install time:

Field Meaning
slug The plugin's identifier (and its /api/<slug>/* + /<slug> mount)
data.collections The collections it owns in the platform's Vault
ui.pages The site pages the core mounts from the bundle
capabilities.provides / consumes Capabilities it offers to / needs from other plugins (over the event bus)
min_core The minimum core version it requires
rbac.roles The RBAC roles + permissions it declares

Two consequences worth knowing:

  • Compatibility is enforced. If a plugin's min_core is newer than your core, the core refuses to install it and tells you to update. See Updating.
  • A plugin carries its own roles. It declares them in rbac.roles and the core registers them under the plugin:<slug> scope at install time. Which of those roles are billable seats is set by the marketplace catalog (railbase.app), not inferred from the manifest. See Licensing & seats.

Why data-resident, not compiled-in or .so files?

A plugin is data the platform executes, not a program that re-embeds the platform. Compiling each plugin into its own binary inverts that relationship, and native .so/plugin loading is platform-locked and ABI-fragile across Go versions. Shipping plugins as encrypted JS run in the core's goja runtime gives you cross-platform artifacts, at-rest encryption (the bundle is never a decompilable executable on disk), and runtime install/update/remove — without trusting unverified code.

Note

Internal dev/embedder builds are not a separate runtime, not a distribution channel, and not how customers obtain plugins — every customer plugin arrives as a signed, encrypted bundle through the marketplace.

Was this page helpful?Thanks for your feedback!