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

import { defineType } from '@estokad/schema'

defineType({
  name: 'article',
  title: 'Article',
  isSingleton: false,
  fields: {
    title: { kind: 'text', required: true, max: 200 },
    slug: { kind: 'slug', source: 'title', unique: true },
    body: { kind: 'richText' },
    publishedAt: { kind: 'dateTime' },
  },
})

The required keys are name and fields. Everything else is optional with sensible defaults — title falls back to a humanised name, isSingleton defaults to false, isEmbedded defaults to false.

Field syntax

Each field key in the fields object is the field name. The value is one of:

// Inline shorthand — kind plus options
{ kind: 'text', required: true, max: 80 }

// Reference fields point at other types by name
{ kind: 'reference', to: ['author'] }

// Asset fields can constrain by mime type
{ kind: 'asset', accept: ['image/*'] }

// Enum fields list options
{ kind: 'enum', options: ['draft', 'review', 'published'] }

Field names must be camelCase. Field types are checked at compile time; passing kind: 'enum' without options is a TypeScript error.

Embedded types

Use defineEmbedded() for inline types that don't have their own table — sub-objects inside an entry, repeating components, etc.

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

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

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

Embedded types are stored as JSONB on the parent row. They are not addressable through references and don't have their own URLs.

Singletons

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

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

The Studio renders singletons in their own list at the top of the Library; you cannot create a second one.

Locales (multi-locale module)

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

fields: {
  title: { kind: 'text', required: true, localizable: true },
  slug:  { kind: 'slug', source: 'title', localizable: true },
  body:  { kind: 'richText', localizable: 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; localisable 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 full IR is documented in docs/schema-system.md in the source repo. The exhaustive list of options per field type ships with the TypeScript types; auto-completion in your IDE is the canonical reference.