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 (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.