Relations
Link entities with single, many-to-many, and polymorphic relations.
Relations connect entities to each other. CMS supports three types of relations, all defined with the same relation() helper.
Single relations
A single relation stores a foreign key column on the entity’s table.
import { entity, text, relation } from "@cms/config";
const authors = entity("authors", {
fields: [text("name", { required: true })],
});
const posts = entity("posts", {
fields: [
text("title", { required: true }),
relation("author", { to: authors }),
],
});
This creates an author_id column on the posts table referencing authors.id.
In the API, an unresolved single relation returns just the ID:
{ "id": "...", "title": "My Post", "author": "author-uuid" }
Many-to-many relations
Set multiple: true to create a junction table.
const posts = entity("posts", {
fields: [
text("title", { required: true }),
relation("tags", { to: tags, multiple: true }),
],
});
This creates a posts_tags junction table with sourceId, targetId, and position columns. Position preserves insertion order. The database enforces referential integrity — deleting a tag automatically removes it from all posts.
Do not use
array()to hold multiple references — wrapping a relation in an array stores IDs as JSONB, losing referential integrity, cross-entity filtering, and resolution. See Handling multiple relations.
Unresolved multiple relations return an array of IDs:
{ "id": "...", "title": "My Post", "tags": ["tag-1", "tag-2"] }
Polymorphic relations
Pass an array of entities to to for polymorphic relations:
relation("link", { to: [pages, posts, externalLinks] })
The target entity type is stored alongside the ID.
Resolving relations
By default, relations return raw IDs. Use the resolve query parameter to expand them into full objects:
GET /api/posts?resolve[author]=*
GET /api/posts?resolve[author]=name,bio
GET /api/posts?resolve[tags]=title
Resolved response:
{
"data": {
"id": "...",
"title": "My Post",
"author": {
"id": "author-uuid",
"name": "Pedro",
"bio": "Design engineer"
}
}
}
Nested resolution
You can resolve relations on resolved entities (max 2 levels deep):
GET /api/posts?resolve[author]=name,avatar&resolve[author.avatar]=url,alt
Resolution behavior
resolve[field]=*— all fields on the related entityresolve[field]=name,bio— only specified fields (id is always included)- Multiple relations are resolved via junction table JOINs, ordered by
position - Resolution uses batched
WHERE id = ANY(...)queries — never N+1 - Unresolved relations on resolved entities return raw IDs unless explicitly resolved
Filtering across relations
You can filter by fields on related entities using dot notation:
GET /api/posts?filter[author.name]=Pedro
This generates a subquery that joins to the related table. See Querying for more on filtering.
Previous
Fields
Next
Querying