Railbase
GPTClaude

Defining your schema

Declare collections, fields, relations, and access rules with the Go DSL.

Updated

Video guide —watch on YouTube ↗

Your data model is Go code. You declare collections (documents in Vault, not SQL tables) and their fields with a fluent builder, register them from an init(), and Railbase generates the storage, the REST API, the realtime topics, and the typed client from that one definition.

A collection

package schema

import s "github.com/railbase/railbase/pkg/railbase/schema"

var Posts = s.Collection("posts").
    Field("title", s.Text().Required().MaxLen(200)).
    Field("body", s.RichText()).
    Field("status", s.Status("draft", "published").Default("draft")).
    Field("author", s.Relation("_users").Required().CascadeDelete()).
    Index("idx_status", "status").
    ListRule("status = 'published' || @request.auth.id != ''").
    ViewRule("status = 'published' || @request.auth.id != ''").
    CreateRule("@request.auth.id != ''").
    UpdateRule("@request.auth.id = author").
    DeleteRule("@request.auth.id = author")

func init() { s.Register(Posts) }

id, created, and updated are added automatically — don't declare them.

Fields

Beyond the primitives — Text() Number() Bool() Date() Email() URL() JSON() Select(...) MultiSelect(...) File() Files() Relation(t) Relations(t) Password() RichText() — the DSL ships a large library of domain types that carry validation and rendering: Currency() Finance() Money… Address() Tel() PersonName() Slug() SequentialCode() Status(...) Priority() Rating() Tags() Country() Percentage() Coordinates() IBAN() Duration() and more.

Chain modifiers on any field:

s.Finance().Required().Min("0.01")          // money — use Finance, never Number().Float()
s.Slug().From("title").Unique()
s.SequentialCode().Prefix("PO-").Pad(5)      // PO-00001, PO-00002, …
s.Text().FTS()                               // full-text searchable

Important

Use Finance() for monetary values, not Number().Float() — floats lose precision on money. The domain types exist so you don't reinvent these rules.

Indexes & constraints

.Index("idx_status", "status")
.UniqueIndex("idx_po_number", "tenant_id", "po_number")   // needs .Tenant()

Indexing tenant_id only validates on a collection that also opts into tenancy with .Tenant() — the column doesn't otherwise exist, and Register panics with "index references unknown column". Always lead a tenant-scoped unique index with tenant_id.

Row-level checks (invariant expressions evaluated on every write) are available to the live schema engine. The public Go DSL exposes the stable field, index and rule surface; use the admin schema editor for checks until the checks builder is part of the public DSL.

Access rules

Every collection has five rules — ListRule, ViewRule, CreateRule, UpdateRule, DeleteRule — written in a small expression language (@request.auth.id != '', @request.auth.id = author, status = 'published').

Caution

Secure by default. An operation with no rule set is locked (server-only), not public. Call .PublicRules() or set explicit rules to open an endpoint. Forgetting a rule fails closed, never open.

Mixins

Common behaviours attach with one call:

Mixin Adds
.Tenant() a tenant_id field + automatic per-tenant row scoping for multi-tenant apps
.SoftDelete() soft delete + restore (records hidden, not destroyed)
.Audit() append to the tamper-evident audit log on every change

Note

End-user sign-in: use the built-in _users. The core guarantees a built-in auth-collection named _users (email, password_hash, verified, token_key, last_login_at, name) — it is the default owner for API tokens / SCIM and the target of /api/collections/_users/auth-signup and auth-with-password. You don't declare it; relate to it with s.Relation("_users").

You cannot register your own _users (or any _-prefixed name): the DSL validator reserves the leading underscore for system collections and Register panics on it. When you need a separate identity pool, declare an auth collection under a plain name — s.AuthCollection("staff"), "customers", etc. Its auth endpoints then live at /api/collections/<name>/auth-*; an unregistered name (e.g. a bare users) returns 404. .AuthCollection() and .Tenant() can't be combined — the validator rejects it.

After editing the schema, generate and apply a migration — see Migrations.

Was this page helpful?Thanks for your feedback!