A content type is a named collection of field definitions. Creating one generates a matching set of GraphQL queries and mutations; changing one regenerates the schema automatically.
Defining a type
The apiId must match ^[a-zA-Z][a-zA-Z0-9_]*$ and cannot collide with a reserved name or another type’s generated GraphQL fields.
Field types
| Type | Stores | Notes |
|---|
string | short text | |
text | long text | |
richtext | long text | your frontend decides how to render it |
int | integer | |
float | number | |
boolean | true/false | |
datetime | ISO-8601 string | |
json | any JSON value | not filterable or unique |
enum | one of values | requires a values array |
media | URL string | bring your own storage (R2, S3, Cloudinary) |
relation | entry id(s) | requires target; add many: true for a list |
Field options
| Option | Effect |
|---|
required | Must be present on create and at publish time. |
unique | Enforced by a database index — duplicate values are rejected. |
indexed | Adds an index so filtering on the field stays cheap. |
default | Applied when the field is omitted. Validated against the field type. |
Mark any field you filter on frequently as indexed. On Cloudflare D1, an unindexed filter scans every entry of that type, which counts against your daily rows-read budget.
Relations
Relations store the target entry’s id (or an array of ids when many). They resolve to the related entry type in GraphQL, with per-request batching so a list query never triggers N+1 lookups.
Deleting a content type deletes all of its entries and is refused while another type has a relation pointing at it. Remove those relations first.
Validation
On every create and update, the payload is checked against the field definitions: unknown fields are rejected, each value is type-checked, required and enum rules are enforced, unique values are checked against the database, and relation targets must exist.