Content · 4 / 23
Content model
Content types are declared in your repository, generate real Postgres tables, and the editor is built from the same file.
export const route = collection('route', { publishable: true, fields: { title: field.text({ required: true, localized: true }), body: field.richtext({ localized: true }), fare: field.money({ min: 0 }), featured: field.boolean(), departsOn: field.date(), region: field.select({ options: ['alps', 'coast'] }), tags: field.multiselect({ options: ['night', 'scenic'] }), }}); How it works
- 01
Declare a content type and its fields in TypeScript.
- 02
Each type becomes its own Postgres table.
- 03
The editor, the API and the permission checks follow from that one file.
- Field types
- 12
- Storage
- One Postgres table per type
- Editor tabs
- Content, Access, History
In depth · For the engineer who signs off
Your content model is code
Content types are TypeScript in your codebase: they generate real Postgres tables, go through review, and roll back like any other change.
Declare it, and the table follows
A content type is a collection() or single() call with its fields. Each becomes a content_<name> table, and the admin form is generated from the same file — add a field and it is in the editor on the next boot.
A collection file on the left and the Postgres table it generates on the right, column by column.
publishable: true adds status and published_at. Twelve field types
Show the table
| Field | What it stores |
|---|---|
| text | A string, with optional length limits. |
| richtext | A structured document in jsonb. |
| number | Integer or decimal, with an optional range. |
| money | An exact amount, never a float. |
| boolean | True or false. |
| date | A calendar date. |
| datetime | A moment, with its time zone. |
| select | One value from a list you define. |
| multiselect | Several values from that list. |
| json | An arbitrary document. |
| relation | A link to another type, four cardinalities. |
| media | One or several assets from the media library. |
Four cardinalities
| Shape | How it is declared |
|---|---|
| many-to-one | field.relation({ to }), a foreign key on this row. |
| one-to-many | The inverse side, declared with relationInverse. |
| one-to-one | A single relation with unique: true. |
| many-to-many | many: true, an ordered join table. |
Adding a field is an ordinary change
-
Edit the collection file
By hand, or with the schema builder in development.
-
Generate the migration
The SQL lands in your repository as a file you can read.
-
Review it
The schema change arrives in the same pull request as the code that uses it.
-
Deploy and migrate
Rolling back is rolling back a deploy.
Questions
Real tables, or one JSON blob?
Real tables: a column per scalar field, foreign keys and join tables. Query them with psql and join them to the rest of your database.
Can non-engineers change the schema in production?
No. Everything about an entry is editable in the browser; the shape of an entry is a code change.