Content Engine

Database & Migrations

How the Content Engine maps content models to PostgreSQL tables using createContentModel, handles system columns, and executes Drizzle Kit migrations.

Content Engine content types compile directly into standard PostgreSQL database tables via Drizzle ORM.

Prerequisites & Context

Before setting up database tables, you must have a content type definition (e.g. articleContentType) created using defineContentType in src/content/article.ts:

src/content/article.ts
import { defineContentType, field } from '@vitnode/core/content'

// 1. Client-safe definition (imported by both API and AdminCP)
export const articleContentType = defineContentType({
  id: 'example.article',
  tableName: 'example_articles',
  fields: {
    title: field.text({ required: true }),
    code: field.text({ required: true }),
  },
})
  • createContentModel(definition, options): A backend utility from @vitnode/core/content/server that converts a client-safe content definition into a server-side Drizzle ORM model.
  • articleContent: The compiled model object containing .table (raw Drizzle table), .service(c) (CRUD helper), and .schemas (Zod schemas).

Step-by-Step Database Setup

Step 1: Create Database Model File and Export Tables

Create your model file under src/database/articles.ts. Import createContentModel from @vitnode/core/content/server and your definition articleContentType:

src/database/articles.ts
import { createContentModel } from '@vitnode/core/content/server'
import { articleContentType } from '@/content/article'
import { example_categories } from './categories'

// Compile client definition into a server Drizzle model
export const articleContent = createContentModel(articleContentType, {
  references: {
    category: () => example_categories.id,
  },
})

// Export raw Drizzle tables for migration discovery
export const example_articles = articleContent.table

// When localization is enabled, export the translation table
export const example_articles_translations = articleContent.translationTable

// When to-many relations or repeatables are used, export junction and child tables
export const example_articles_categories =
  articleContent.advancedTables.junctions.categories
export const example_articles_faq =
  articleContent.advancedTables.repeatables.faqItems

Always Export Generated Tables

Drizzle Kit discovers database tables by scanning the exported members of built files in dist/src/database/*.js. Omitting an export for table, translationTable, or any junction/repeatable table in advancedTables means Drizzle Kit will not discover it and will omit it from the generated SQL migration.

Step 2: Add Database Indexes

Add single or composite indexes inside your content definition file src/content/article.ts:

src/content/article.ts
export const articleContentType = defineContentType({
  id: 'example.article',
  tableName: 'example_articles',
  fields: {
    title: field.text({ required: true }),
    code: field.text({ required: true }),
    status: field.enum({ values: ['draft', 'published'] }),
  },
  indexes: [
    { on: ['status', 'createdAt'] }, // Composite index
    { on: ['code'], unique: true }, // Unique constraint
  ], 
})

Step 3: Run Database Migrations

Compile your plugins and apply schema changes:

bun run build:plugins && bun run db:migrate

Automatic System Columns

When createContentModel compiles a model, it automatically includes standard system columns on the base table:

ColumnPostgres TypeDescription
idserial / integerPrimary key identifier
createdAttimestampAuto-populated creation timestamp
updatedAttimestampAuto-updated modification timestamp

If publication is enabled, status (varchar(32)) and publishedAt (timestamp) columns are added.
If editorial is enabled, a version (integer) column is added for optimistic concurrency control.

When localization is enabled, the secondary translation table ({tableName}_translations) carries its own system columns:

ColumnPostgres TypeDescription
itemIdintegerForeign key to the base table id (composite PK)
languageIdintegerForeign key to core_languages.id (composite PK)
versionintegerPer-locale version for concurrency control
createdAttimestampCreation timestamp of this translation
updatedAttimestampModification timestamp of this translation
statusvarchar(32)Translation publication status (with publication)
publishedAttimestampTranslation publication timestamp (with publication)