Defining your schema
Declare collections, fields, relations, and access rules with the Go DSL.
Updated
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.