Defining your schema
Declare collections, fields, relations, and access rules with the Go DSL.
Updated
Your data model is Go code. You declare collections (think 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")
Row-level checks (invariant expressions evaluated on every write) are
defined per collection in the admin schema editor; the Go DSL's .Checks(...)
seam exists but its CheckSpec argument type isn't exported from
pkg/railbase/schema yet.
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() |
tenant_id + row-level isolation 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"). To own its shape,
register your own s.AuthCollection("_users") and the core uses yours instead.
The auth endpoints exist for registered auth collections only — nothing
registers a plain users (no underscore), so /api/collections/users/auth-*
returns 404 unless your schema declares it; prefer the built-in _users (or a
distinct name like staff) to avoid confusion with the pre-_users holdover.
.AuthCollection() and .Tenant() can't be combined — the validator
rejects it.
After editing the schema, generate and apply a migration — see Migrations.