Schema design
Collection schemas are durable API contracts. Model fields for validation and policy, not merely for how one screen happens to look.
Example: issue tracker
{
"name":"issues",
"fields":[
{"name":"title","type":"text","required":true},
{"name":"status","type":"select","required":true,"default":"open"},
{"name":"priority","type":"number","default":2},
{"name":"owner_id","type":"relation","required":true},
{"name":"due_at","type":"datetime"},
{"name":"metadata","type":"json","default":{}}
]
}Naming
Names begin with a lowercase ASCII letter, contain lowercase letters, digits and underscores, and are at most 63 characters. Treat them like API identifiers: prefer due_at over a display label such as “Due date”. Leading underscores and reserved names are rejected.
Choose field types deliberately
- Use
selectfor a controlled state such as open/closed, not free text. - Use
datetimefor comparable timestamps and normalize to UTC. - Use
relationwhen a value identifies another record, while remembering target-aware physical foreign keys are not yet generated. - Use
jsonfor genuinely variable metadata, not as an escape hatch for every field. - Use
uniqueonly for application invariants; it has write and migration costs.
Defaults and required fields
Defaults must be valid JSON and match the field type. A required field with a sensible default can make additive evolution safe. A required field without a default needs a data backfill before existing rows can satisfy it.
Design for access rules
Store policy-relevant values such as owner_id, team_id or visibility as explicit fields. Hiding ownership inside a JSON blob makes rules, indexes and migrations harder to reason about.
Metadata and storage
Trestle assigns stable internal IDs to collections and fields. User names never become SQL identifiers. The complete metadata definition and physical SQLite table change in one transaction.