Schemas & records
A schema is a content type — Articles, Products, Team members. It declares fields; the server enforces them on every write. A record is one document of that type, stored in your own database, editable in the dashboard and readable over a generated REST API.
Defining a schema
Create one under Schemas with a name (becomes part of the API and the collection name) and fields. Each field has a name, a type, and optional rules.
Field types
| Type | Holds | Extra rules |
|---|---|---|
text, textarea | Short / long text | minLength, maxLength |
richtext | Formatted HTML content | — |
email | An email address (validated, lowercased) | — |
url | Absolute URLs or site-relative paths (/shop) | — |
number | A number | min, max |
boolean | true / false | — |
date, datetime, time | Points in time | — |
enumeration | One of a fixed list | options |
json | Arbitrary structured data | — |
media | A reference to a media file (or a list) | — |
relation | A reference to a record in another schema | target schema, single or many |
component | A nested group of fields, defined once as a model | — |
dynamiczone | An ordered list of component instances | allowed models, min/max items |
Per-field flags: required and unique.
Validation is a guarantee, not a suggestion
Every write — from the dashboard, the records API, or a flow's record step — passes the same checks:
- Required, types, ranges are enforced server-side; failures come back as per-field errors (
422, with anissuesarray). - **
uniquefields are backed by real database indexes** (nulls exempt), so even two simultaneous writes cannot both slip through. On data migrated with pre-existing duplicates the index cannot build; the pre-write check still runs. - Relations must point at records that exist. A typo'd or stale ID is rejected with a per-field error instead of storing a dangling reference. (
mediareferences are exempt — files are deletable by design.) - Unknown fields are dropped, not rejected — removing a field from a schema doesn't break clients still sending it.
Models and dynamic zones
Models are reusable field groups (a "CTA" with title, text, link). A component field embeds one; a dynamiczone field holds an ordered list of instances of allowed models — the classic page-builder pattern, validated instance by instance.
Working with records
The Records view lists, filters, creates and edits records; you can customise which columns show and bulk-delete selections. Every create, edit and delete writes a history entry (before and after, who, when) — the History tab is your undo trail, and it retains deleted content on purpose.
The records API
Every schema gets a REST API, documented per-schema under its Docs tab with examples written against your own fields. Authenticate with a session or an API key (Authorization: Bearer esc_…).
GET /api/projects/{project}/schemas/{schema}/records
POST /api/projects/{project}/schemas/{schema}/records
GET /api/projects/{project}/schemas/{schema}/records/{id}
PATCH /api/projects/{project}/schemas/{schema}/records/{id}
DELETE /api/projects/{project}/schemas/{schema}/records/{id}The query language
All on the list endpoint (and where sensible, the single-record one):
| Parameter | Example | Meaning |
|---|---|---|
page, limit | ?page=2&limit=25 | Pagination (limit ≤ 100, default 10) |
search | ?search=launch | Text search across text fields |
sort | ?sort=-createdAt | - prefix for descending |
filter[field] | ?filter[status]=live | Partial match on text, exact elsewhere |
exactFilter[field] | ?exactFilter[title]=Hello | Exact match always |
filter[field][op] | ?filter[views][$gte]=100 | Comparison operators |
expand | ?expand=author or expand=* | Embed related records |
fields / omit | ?fields=title,status | Shape the response |
Allowed operators: $eq $ne $gt $gte $lt $lte $in $nin $exists $size $all (list operators take comma-separated values). $regex is deliberately not available, and field keys may never contain $ — the query language cannot be used to smuggle raw database operators.
Relation expansion
?expand=author populates the related record under each record's expand key in one extra query per target schema — not one per record. expand=* expands every relation field.
Records + flows
Endpoints and events read and write records through the same validation with dedicated steps — query records (with expand), create record and update record — so "combine two schemas and return a shaped response" needs no server code.
Standalone schemas
Projects migrated from v1 may carry standalone schemas — an older flavour with its own history and settings. They resolve through the same APIs and live under their own dashboard section; new projects only need regular schemas.