Relations & Advanced Modeling
Connect content types with foreign keys, junction tables, field groups, and repeatable child records.
Content Engine uses real relational modeling rather than unstructured JSON blobs. To-many relations generate indexed junction tables, repeatables generate child tables with foreign keys, and groups map to structured columns.
Quick start: To-One Relation
Connect an article to a single category:
1. Declare the Field
import { defineContentType, field } from "@vitnode/core/content"
import { categoryContentType } from "./category"
export const articleContentType = defineContentType({
id: "blog.article",
tableName: "blog_articles",
fields: {
title: field.text({ required: true }),
category: field.relation({
required: true,
onDelete: "restrict",
target: () => categoryContentType,
}),
},
})2. Connect Drizzle References
When a relation targets another content type, provide the referenced primary key column in createContentModel:
import { createContentModel } from "@vitnode/core/content/server"
import { articleContentType } from "../content/article"
import { blogCategoriesTable } from "./categories"
export const articleContent = createContentModel(articleContentType, {
references: {
category: () => blogCategoriesTable.id,
},
})
export const blogArticlesTable = articleContent.tableSelf-referencing relations (self: true), field.user, and field.file resolve their references automatically and do not require entries in references.
Relation Shapes
| Shape | Declaration | Database Structure |
|---|---|---|
| To-One | field.relation({ target }) | Foreign key column on base table (categoryId). |
| To-Many | field.relation({ target, multiple: true }) | Auto-generated junction table ({tableName}_{field}). |
| Self-Referencing | field.relation({ self: true }) | Foreign key referencing the same table's id. |
| User Relation | field.user({ multiple?: boolean }) | FK to core_users.id (or junction table if multiple: true). |
| File Relation | field.file({ multiple?: boolean }) | FK to core_files.id (or junction table if multiple: true). |
To-Many Relations & Junction Tables
Support multi-category selection or ordered co-authors:
fields: {
categories: field.relation({
multiple: true,
min: 1, // Enforce at least 1 selected
onDelete: "cascade", // "cascade" or "restrict" (cannot be "set null" on junction)
target: () => categoryContentType,
}),
authors: field.user({
multiple: true,
ordered: true, // Preserves drag-and-drop order
}),
}Exporting Junction Tables for Migrations
To-many relations generate junction tables accessible via model.advancedTables.junctions. Export them so Drizzle Kit generates their schema migrations:
import { createContentModel } from "@vitnode/core/content/server"
import { postContentType } from "../content/post"
import { blogCategoriesTable } from "./categories"
export const postContent = createContentModel(postContentType, {
references: {
categories: () => blogCategoriesTable.id,
},
})
export const blogPostsTable = postContent.table
export const blogPostsCategoriesJunctionTable =
postContent.advancedTables.junctions.categoriesField Groups (field.group)
Group related fields into a single nested object:
fields: {
seo: field.group({
fields: {
metaTitle: field.text({ maxLength: 70 }),
metaDescription: field.textarea({ maxLength: 160 }),
noIndex: field.boolean({ defaultValue: false }),
},
}),
}Columns are created with snake_case prefixes on the base table (seo_meta_title, seo_meta_description, seo_no_index).
Repeatable Child Rows (field.repeatable)
Create structured arrays backed by a real child database table:
fields: {
faqItems: field.repeatable({
min: 1,
max: 10, // Defaults to 100
fields: {
question: field.text({ required: true }),
answer: field.textarea({ required: true }),
},
}),
}Exporting Repeatable Tables for Migrations
Repeatable child tables are stored under model.advancedTables.repeatables. Export them alongside your base table:
export const blogPostsTable = postContent.table
export const blogPostsFaqItemsChildTable =
postContent.advancedTables.repeatables.faqItemsEach repeatable row carries id, itemId (FK cascading to the parent record), position, createdAt, and the child field columns.
field.relation Options
Prop
Type
Learn More
Localization & Translations
Step-by-step guide to building multi-language content models with localized fields, translation tables, independent workflows, and localized public APIs.
Content Delivery and SEO
Deliver Content Engine records from a plugin with canonical metadata, slug redirects, hreflang, and XML sitemap support.