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.