Content types & fields
The First content type page is the basic tour: create a type, pick fields, wire up references and localization. This page is the reference: inheritance, interfaces, polymorphic blocks, permissions, and what happens under the schema’s hood.
Inheritance (Extends)
A type can inherit another type’s fields. In the type editor, pick a
Parent content type (under Inheritance) — the child receives all of the
parent’s fields plus its own. Example: blog-post extends page — it gains
title, slug, status, seo, blocks, then adds excerpt,
published_at, author, tags.
Inheritance rules:
- Single and chainable — a type inherits one parent, but the parent may
itself inherit:
landing-page → page → …. Depth is unbounded. - Child overrides parent — if the child declares a field with the same name, it replaces the parent’s in place (order preserved).
- Field order is stable — parent fields first (in their declared order), then the child’s own.
- Cycles are rejected —
A extends B extends Afails withCIRCULAR_INHERITANCE. - Parent must exist — pointing at a non-existent slug returns
PARENT_NOT_FOUND. - Extends is immutable after creation — the parent is locked in once.
- A child may have zero own fields —
landing-pageextendspageand adds nothing: the whole page is composed of blocks.
Inherited fields land not only in the GraphQL object type but also in filters
and sorting. blogPosts(where: { status: { eq: "published" } }) works
precisely because status is inherited from page.
Interfaces (Node, Timestamped)
Every type automatically implements two GraphQL interfaces:
| Interface | Fields |
|---|---|
Node |
id: ID! |
Timestamped |
created_at: DateTime, updated_at: DateTime |
These fields are added by the server — you don’t declare them in the type’s
schema. id is generated on record creation; created_at and
updated_at are set on write.
A third interface — Entity (id, slug, name) — exists but is not
auto-added to types; it’s used for internal typing.
Polymorphic blocks
A Content type field + Make array with no target type is a polymorphic
block list. The blocks field on page works this way: one page can hold
blocks of any type (Hero, Text, Gallery, CTA, etc.), and element order is
preserved.
In GraphQL such a field gets the type [Block], where Block is a union of
every type in the schema. Query:
query {
pages {
title
blocks {
... on HeroSection { title subtitle }
... on TextBlock { body }
... on Gallery { images { url } }
}
}
}
Polymorphic-block constraints:
- No filtering or sorting — the
blocksfield never appears in<Type>Whereor<Type>OrderByField. - Cannot be localized — the server rejects it
(
LOCALIZED_POLYMORPHIC_BLOCKS_UNSUPPORTED). The block list is shared across languages. - Field names must be compatible — every type sits in the
Blockunion, so same-named fields across types must agree on their GraphQL type. Else:BLOCK_UNION_FIELD_TYPE_CONFLICT.
The union is live: it contains every type in the schema, so a block type created later is queryable through blocks right away (give the async schema rebuild a second or two). You also don’t have to enumerate fragments by hand — a bare blocks { } selection is auto-expanded by the server into per-type scalar selections with __typename. And if two ALREADY-EXISTING types expose a same-named field with different scalar shapes (say Int vs Float), queries keep working: the server aliases the conflicting leaf per union member. The create-time check above guards NEW definitions; older collisions are tolerated at query time, not broken.
If you need a list of references to one concrete type (e.g. tags), specify the target type — the field becomes a regular reference array that can be filtered and localized.
Select fields
A Select (select) is an enum field: the value is chosen from a
fixed list of options. It is exposed as String in GraphQL, but the server
enforces membership — you cannot store an arbitrary string.
Options are configured right in the field editor. When the type is set to Select, an Options editor appears. Each row has:
- Value (stored) — what gets saved to the DB (
draft,published). - Label (shown) — what the editor sees in the admin (
Draft,Published).
A Default value is also picked — it is pre-filled on new records.
Example: the status field on page stores draft or published.
Trying to write archive — the server returns
SELECT_VALUE_NOT_ALLOWED.
If a Select has zero options configured, enforcement is disabled (the field behaves as free text). This is for backward compatibility with legacy fields created before this mechanism existed.
Reserved names
A type’s GraphQL name is the PascalCase of its slug (blog-post →
BlogPost). Names the schema reserves for itself cannot be used:
Node, Entity, Timestamped, EntityDefinition, PropertyDefinition,
Condition, ValidationRule, Item, GenericItem, APIKey,
APIKeyMutation, Query, Mutation, CreateEntityInput,
UpdateEntityInput, PropertyInput, ConditionInput,
ValidationRuleInput, AuthPayload.
If the PascalCase of your slug collides with one of these, the server rejects the type.
Per-type permissions
Every type carries a permission matrix — which roles can read, create, update, and delete its records. Defaults:
| Role | Read | Create | Update | Delete |
|---|---|---|---|---|
| viewer | yes | — | — | — |
| editor | yes | yes | yes | — |
| admin / owner | yes | yes | yes | yes |
Permissions live on the type’s permissions field and are edited from the
“Permissions” page in the admin. See Roles & access for the
full guide.
Under the hood
Records are stored as JSONB in the entity_data table. The type schema (the
field definitions) lives separately, in entity_definitions. When you change
the schema:
- The server validates the type (reserved names, inheritance cycles, Block-union conflicts).
- Saves the new version to
entity_definitionsand bumps the schema version. SchemaManagerrebuilds the GraphQL schema from the current definitions.- The new schema takes effect atomically — queries in flight finish on the old one, new queries hit the new one.
Add a field — it’s in the API. Remove it — it’s gone. No data migration: JSON holds the shape, the definition describes it.
What’s next
- GraphQL queries — singular and plural, variables, locale.
- Filtering & sorting — Where, operators, OrderBy.