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
import { createContentModel } from "@vitnode/core/content/server"
import { articleContentType } from "../content/article"
import { blog_categories } from "./categories"
export const articleContent = createContentModel(articleContentType, {
references: {
category: () => blog_categories.id,
},
})Relation Shapes
| Shape | Declaration | Database Structure |
|---|---|---|
| To-One | field.relation({ target }) | Foreign key column on table |
| To-Many | field.relation({ target, multiple: true }) | Auto-generated junction table |
| Self-Referencing | field.relation({ self: true }) | FK to same table (e.g. parent category) |
| User Relation | field.user({ multiple?: boolean }) | FK / junction to core_users |
| File Relation | field.file({ multiple?: boolean }) | FK / junction to core_files |
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
target: () => categoryContentType,
}),
authors: field.user({
multiple: true,
ordered: true, // Preserves drag-and-drop order
}),
}Field 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 as seo_meta_title, seo_meta_description, and seo_no_index on the main table.
Repeatable Child Rows (field.repeatable)
Create structured arrays backed by a real child table:
fields: {
faqItems: field.repeatable({
min: 1,
max: 10,
fields: {
question: field.text({ required: true }),
answer: field.textarea({ required: true }),
},
}),
}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 & SEO
Deliver Content Engine records over the web with TanStack Start, automatic 308 redirects, plugin route manifests, and rich SEO metadata.