Data & multi-tenancy
The single-file vault store, collections, and row-level tenant scoping.
Updated
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
.vaultfile; include the storage directory if you useFile()/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.