defineType()

The authoring API for content types. Every type lives in a schemas/*.ts file, exports a single defineType() call, and is re-exported from schemas/index.ts.

Signature

fields is an array of field objects, each carrying its own name and discriminated on type:

import { defineType } from '@estokad/schema'

defineType({
  name: 'article',
  title: 'Article',
  isSingleton: false,
  fields: [
    { name: 'title', type: 'text', required: true, max: 200 },
    { name: 'slug', type: 'slug', source: 'title' },
    { name: 'body', type: 'richText' },
    { name: 'publishedAt', type: 'datetime' },
  ],
})

The required keys are name and fields. Everything else is optional with sensible defaults — title falls back to a humanised name, isSingleton and isEmbedded default to false. Set displayField to the field that names an entry in the Library (defaults to the first text field); it's denormalised onto the entry's title, so it doesn't have to be a field literally called title.

Field syntax

Every field is { name, type, ...options }. Field names are camelCase; the type is checked at compile time (e.g. an enum without options is a TypeScript error).

// Text — max/min length, optional pattern (RegExp or string)
{ name: 'headline', type: 'text', required: true, max: 80 }

// Slug — derived from a source field
{ name: 'slug', type: 'slug', source: 'headline' }

// Reference — points at another type by name. `to` is a string;
// use an array only on a referenceList to allow multiple target types.
{ name: 'author', type: 'reference', to: 'author' }
{ name: 'related', type: 'referenceList', to: ['article', 'source'] }

// Asset — constrain by mime type / size
{ name: 'hero', type: 'asset', accept: ['image/*'], maxSize: 5_000_000 }

// Enum — list the allowed options
{ name: 'status', type: 'enum', options: ['draft', 'review', 'published'] }

Embedded types

Use defineEmbedded() for inline types stored as JSONB on the parent row — sub-objects, repeating components. They aren't addressable through references and have no URLs.

import { defineEmbedded, defineType } from '@estokad/schema'

const seo = defineEmbedded({
  name: 'seo',
  fields: [
    { name: 'title', type: 'text', max: 60 },
    { name: 'description', type: 'text', max: 160 },
  ],
})

defineType({
  name: 'page',
  fields: [
    { name: 'title', type: 'text', required: true },
    { name: 'seo', type: 'embedded', of: seo },
  ],
})

embedded holds a single object. For a repeatable list of structured items, model them as their own type and cite with referenceList (there is no array-of-embedded field).

Singletons

A singleton type has exactly one entry per space — site settings, navigation, header, footer. Mark isSingleton: true:

defineType({
  name: 'siteSettings',
  isSingleton: true,
  fields: [
    { name: 'siteName', type: 'text', required: true },
    { name: 'primaryColor', type: 'text', pattern: /^#[0-9a-f]{6}$/ },
  ],
})

The Studio renders singletons in their own list at the top of the Library; you cannot create a second one. Read one back with cms.siteSettings.list({ first: 1 }).

Locales (multi-locale module)

With the multi-locale module active, any field becomes per-locale by adding localized: true:

fields: [
  { name: 'title', type: 'text', required: true, localized: true },
  { name: 'slug', type: 'slug', source: 'title', localized: true },
  { name: 'body', type: 'richText', localized: true },
]

The runtime fetch resolves to the locale you ask for; the Studio shows a locale switcher. Per-locale publish state lets you ship the French translation while the Dutch is still being reviewed.

Modules

Some field options require a paid module. Cross-space references require cross_space_orchestration; localized fields require multi_locale. The CLI surfaces this on estokad push — pushing a schema that needs an inactive module returns a 412 with the missing module name.

Reference

The exhaustive list of options per field type ships with the TypeScript types — auto-completion in your IDE is the canonical reference, and tsc is the source of truth over any example here.