Railbase
GPTClaude

Building plugins

How plugins are structured, run, and distributed (advanced).

Updated

Video guide —watch on YouTube ↗

Plugins are how functionality is packaged and sold on top of Railbase. This page is an overview of the plugin model for developers; it's an advanced topic — most apps are built directly on the core (see the rest of this section).

A plugin is a data-resident bundle the core runs

The unit a customer receives and the platform runs is a data-resident bundle: an encrypted JavaScript + schema bundle stored in the core's own Vault (the _plugins row), decrypted only at the license gate, and executed in-process in the core's goja runtime. A plugin has no main(), no port, no subprocess, no database of its own, and no run path outside the core — it is data the platform runs, not a program.

The bundle ships:

  • Verbs registered with $app.routerAdd("POST", "/api/<slug>/…", fn) — the core serves them in-process at /api/<slug>/*. There is no reverse proxy and no child process; on install the core reloads its hooks runtime and the plugin's verbs mount immediately — no rebuild, no restart, no extra port.
  • Collections declared as schema_json inside the bundle. The core reconciles them into the shared Vault on install.
  • UI as a declarative bundle/widgets.json descriptor (pages + widgets bound to the plugin's verbs). The core's site shell mounts it at /<slug> on install — the whole plugin (backend + schema + UI) lands in one marketplace action.

The Go Register(app) form — importing the module and compiling it into a self-hosted build — is a dev/embedder build detail only. It is never the unit a customer receives and never a distribution channel. Everything below describes the shipped, data-resident model.

How a data-resident plugin uses the platform

The platform provides everything; the plugin only consumes it through mediated host bindings — never *App internals:

  • Data$app.dao(), the tenant-scoped binding over the platform's shared Vault (single-file CBOR document store, not SQL). There is no per-plugin store and no embedded vault; the DB is the platform's, shared but mediated. Tenant scoping is explicit — stamp tenant_id on write, filter on read.
  • Identitye.auth, the trusted authenticated user/tenant on each request. Never read identity from a client header.
  • Events — publish with $app.realtime().publish(topic, payload) and consume with the $app.onEvent(topic, fn) binding.
  • Jobs — background work via $jobs.
  • Licensing — plan/quota via $app.license.

The core enforces min_core from the catalog (it refuses a plugin that needs a newer core).

Inter-plugin communication

Plugins never import one another — every cross-plugin interaction goes through the platform's event bus. A plugin publishes and subscribes through the bus; there are no Go imports and no direct plugin→plugin calls.

Re-shape an incoming payload with eventbus.Coerce(e.Payload, &out) — the JSON round-trip helper every listener uses (swallow semantics: a malformed payload leaves out zero-valued; validate the fields you need). Two contracts CI enforces across the suite: every subscription must have a live publisher (or a documented allowlist entry), and every topic your plugin.yaml consumes must match a real subscription in your code — aspirational declarations fail the build.

The canonical example is the ledger contract: ap/ar/fa (and htr/inventory) emit gl.journal.post.request, and the gl ledger plugin posts the journal idempotently and replies gl.journal.posted.

For ecosystem value, plugins also expose named capabilities a peer can consume, and the core mediates and runs detach hooks before a provider is removed — always over declared, discoverable contracts, never ad-hoc Go calls.

Distribution

Plugins reach customers only through the marketplace, as signed bundles. railbase.app is the sole orchestrator of purchase, licensing, install, update, and removal — there are no side channels and no manual binary handoff:

  1. The bundle is published to railbase.app (the vendor's distribution server) with its version, content hash, and Ed25519 signature.
  2. A customer buys a subscription; railbase.app issues a lic1 license.
  3. The customer's core pulls the bundle, verifies sha256 + signature against the pinned vendor key, lands the encrypted, license-gated row in its Vault, and runs the decrypted JS in goja. A paid plugin without a valid license installs dormant — its code is never decrypted — and recovers to active automatically once a valid license is bound.

There is no sideloading — see How plugins work and Licensing & seats for the full trust and billing model.

Pricing is a value-metric union — per-seat (seat_monthly) is the default, but a plugin may instead be priced flat_monthly (per company/mo), host_seat_monthly, flat_usage_monthly, or custom.

Note

Authoring and publishing plugins to the marketplace is a vendor/partner process. If you want to distribute a plugin, get in touch via Support.

Was this page helpful?Thanks for your feedback!