Content Engine

Field Reference

Complete guide to adding and configuring field types in Content Engine, including Postgres mappings, Zod validation, and AdminCP controls.

Content Engine provides field descriptors exported from @vitnode/core/content via the field helper. Each field generates a typed Drizzle column, Zod validation schemas, and an interactive AdminCP input automatically.

Supported Field Types

Field MethodPostgres ColumnZod SchemaAdminCP Form Control
field.text()varchar(maxLength)z.string()Text Input
field.textarea()text()z.string()Textarea
field.number({ integer: true })integer()z.number().int()Integer Input
field.number({ integer: false })double precisionz.number()Number Input
field.boolean()boolean()z.boolean()Switch Toggle
field.enum({ values: [...] })varchar(length)z.enum([...])Select Dropdown / Radio
field.dateTime()timestamp()z.date() / ISO stringDate / Time Picker
field.slug()varchar(maxLength)z.string()Slug Generator
field.user()integer() (FK core_users)z.number()User Combobox
field.user({ multiple: true })Junction tablez.array(z.number())Multi-user Combobox
field.file()integer() (FK core_files)z.number()Drag-and-drop File Upload
field.file({ multiple: true })Junction tablez.array(z.number())Multi-file Gallery Uploader
field.relation()integer() (FK target table)z.number()Relation Combobox
field.relation({ multiple: true })Junction tablez.array(z.number())Multi-relation Combobox
field.group({ fields: ... })Prefixed columnsz.object(...)Grouped Card
field.repeatable({ fields: ... })Child tablez.array(...)Repeatable List
field.blocks()jsonbz.array(...)None yet - see Widgets

Field Configurations

Text and Strings

Configure string length, required state, and multi-language support:

plugins/blog/src/content/post.ts
import { defineContentType, field } from "@vitnode/core/content"

export const postContentType = defineContentType({
  id: "blog.post",
  tableName: "blog_posts",
  fields: {
    title: field.text({
      required: true,
      minLength: 3,
      maxLength: 200,
      localized: true,
    }),
    summary: field.textarea({
      maxLength: 500,
      localized: true,
    }),
  },
})

localized: true marks a field for multi-language translation. When localization: { enabled: true } is enabled on the content type, localized field values are stored in the generated {tableName}_translations table rather than on the base table.

Numbers, Booleans, and Enums

Number fields require the integer boolean option. When integer: true, Content Engine generates an integer column in PostgreSQL; when integer: false, it generates a double precision column for floating-point values.

plugins/blog/src/content/post.ts
fields: {
  views: field.number({
    integer: true,
    defaultValue: 0,
    min: 0,
  }),
  rating: field.number({
    integer: false,
    min: 0,
    max: 5,
  }),
  featured: field.boolean({
    defaultValue: false,
  }),
  status: field.enum({
    values: ["draft", "published", "archived"] as const,
    defaultValue: "draft",
  }),
}

Slugs, Dates, and User Relations

Auto-generate slugs from another field, default timestamps to current time, or link users:

plugins/blog/src/content/post.ts
fields: {
  slug: field.slug({
    source: "title",
  }),
  publishedAt: field.dateTime({
    defaultNow: true,
  }),
  author: field.user({
    required: true,
  }),
  contributors: field.user({
    multiple: true,
    ordered: true,
  }),
}
  • source: "title" automatically derives the slug from the title field upon creation. When source is omitted, the slug must be explicitly supplied in the create payload.
  • defaultNow: true configures PostgreSQL and Drizzle to automatically default the column value to now() upon insertion.

Single and Multi-File Uploads

VitNode connects file fields directly to core_files with storage adapters (Local disk, S3, Cloudflare R2):

plugins/blog/src/content/post.ts
fields: {
  // Single cover image (max 5 MB)
  coverImage: field.file({
    maxBytes: 5 * 1024 * 1024,
    allowedExtensions: [".jpg", ".jpeg", ".png", ".webp"],
    allowedMimeTypes: ["image/jpeg", "image/png", "image/webp"],
  }),

  // Gallery of screenshots
  gallery: field.file({
    multiple: true,
    min: 1,
    max: 10,
    maxBytes: 10 * 1024 * 1024,
    allowedExtensions: [".jpg", ".jpeg", ".png", ".webp"],
    allowedMimeTypes: ["image/jpeg", "image/png", "image/webp"],
  }),
}

Automatic Multipart Upload Pipeline

In the AdminCP, AutoFormFile uploads attachments via TanStack Query and standard multipart API routes (POST /api/.../uploads/{field}). Uploaded records are stored in core_files and assigned by ID.


Common Field Options

Field descriptors accept these standard attributes:

Prop

Type


Search, Ordering, and Filter Configuration

searchable and sortable are not field descriptor options. Searching and ordering are configured at the feature level to separate AdminCP management from public API delivery and global search indexing:

1. AdminCP Table (admin.list)

Configure how administrators search and sort the AdminCP data table:

  • admin.list.searchableFields: Array of shared text, textarea, or slug fields searchable via the table search input.
  • admin.list.orderableFields: Array of shared scalar columns administrators can click to sort. System columns (id, createdAt, updatedAt) and publication columns (status, publishedAt) are always allowed.
  • admin.list.defaultOrderBy: The column name string to sort by initially (e.g. 'updatedAt').
  • admin.list.defaultOrder: Sort direction ('asc' | 'desc').

Localized Fields Cannot Be SQL-Sorted on the Base Table

Because localized fields live on the secondary {tableName}_translations table, they cannot be ordered with SQL ORDER BY on base table queries. Therefore, admin.list.orderableFields accepts only shared columns. However, localized fields can be displayed as cells in admin.list.columns and used for admin.titleField (resolved in the editor's language).

2. Public API (publicApi)

Control which fields public consumers can query, search, and sort:

  • publicApi.searchableFields: Columns scanned by the public ?search= parameter.
  • publicApi.orderableFields: Columns accepted by the public ?orderBy= parameter (in addition to publishedAt).
  • publicApi.filterableFields: Columns accepted as exact equality filters in query parameters (e.g. ?category=1).

Synchronize records with the VitNode search and discovery engine:

  • search.titleField: Non-nullable text field used as the search result heading.
  • search.descriptionField: Optional text or textarea field prepended to the indexed body.
  • search.contentFields: Array of fields and group/repeatable leaves concatenated into the full-text search document.

Learn More