Railbase
GPTClaude

Data & multi-tenancy

The single-file vault store, collections, and row-level tenant scoping.

Updated

Video guide —watch on YouTube ↗

Railbase has no external database. Records live in a single encrypted Vault file managed by an in-process engine; uploaded blobs live in the configured storage directory. This keeps operations straightforward: one process, a Vault snapshot, and file storage to back up when you use uploads.

The vault

The store is an encrypted, MVCC document store kept in one file — pb_data/railbase.vault by default. "MVCC" means readers never block writers, so the single-file model still serves concurrent traffic well. The file is encrypted at rest under a key derived from your vault password, which unlocks it at boot. (The separate pb_data/.secret file is not the vault's at-rest key — it is the master secret that signs tokens, seals the audit chain, and keys field-level encryption. Back up both.) See Installation and Security.

What this means for you:

  • No connection string, no DB server, no migrations service. Schema migrations apply automatically on boot.
  • Backups are ordinary file copies. A snapshot is a byte-exact copy of the .vault file; include the storage directory if you use File() / Files() fields — see Backups & restore.
  • Keep it on local disk. Use the VPS's local disk, not a network mount, for correct file locking and durability.
  • A single document can be up to 4 MiB. Documents larger than a storage page are split across pages transparently; past 4 MiB a write is rejected with an explicit error. Store large payloads as files, not document fields. (The REST create endpoint additionally caps a request body at 1 MiB.)

Collections

Data is organized into collections (think tables). The core owns the system collections (users, sessions, audit log, settings); each plugin owns its own. Plugins conventionally prefix their collection names with their slug to avoid collisions, though the platform enforces namespacing at the route/verb/UI level (/api/<slug>/*), not on collection names themselves.

When you uninstall a plugin its collections are left dormant rather than deleted, so reinstalling restores your data. A separate, backup-gated purge permanently removes them — see Installing plugins.

Multi-tenancy

For deployments that serve multiple isolated organizations, Railbase uses row-level tenancy inside the one vault file. Tenants are registered in the built-in _tenants collection — from the CLI:

./railbase tenant create acme
./railbase tenant list

A collection opts into tenancy with the .Tenant() mixin in the schema. Its rows then carry a tenant_id column, and the scope comes from the request: a call with an X-Tenant: <tenant-uuid> header (the TypeScript SDK's rb.setTenant(id)) writes and reads only that tenant's rows — the value is stamped on insert and filtered on read. Roles can likewise be granted per tenant (RBAC).

By default (tenancy.implicit_single_tenant, on) a request with no header resolves into the sole tenant when exactly one exists — so a single-company instance "just works" without every caller setting X-Tenant. Only with zero or several tenants (or the setting off) does a header-less request run at site scope.

Important

There is a membership gate: an authenticated app user who is not an accepted member of any tenant is refused across /api/* with 403 tenant_membership_required (it fails closed). Operator/admin identities and anonymous requests are not gated. So a multi-tenant instance must add users to a tenant (_tenant_members) before they can call the API — a single-tenant instance seeded with its first company (see admin bootstrap --company) satisfies this for members of that company.

Tip

A single-tenant deployment is the common case: seed one company, and callers need no X-Tenant header. Reach for multiple tenants only when you actually host separate organizations on one instance. Backups operate on the whole vault file — i.e. on all tenants at once.

Was this page helpful?Thanks for your feedback!