Copied to clipboard!

This chapter provides a deep dive into Vibe CMS's content architecture, exploring how collections, fields, content items, and translations work together to create a powerful, flexible content management system.

1. Collections Deep Dive

What is a Collection?

A collection in Vibe CMS is a content type definition - a blueprint that defines the structure and schema for a specific type of content. Think of collections as tables in a database or classes in object-oriented programming.

Collection Properties

Collections are defined in the cms_collections table with the following properties:

  • id: UUID primary key
  • project_id: Links collection to a specific project
  • account_id: Links collection to the owning account
  • name: Human-readable display name (e.g., "Blog Posts", "Team Members")
  • slug: URL-friendly identifier (e.g., "blog-posts", "team-members")
  • description: Optional text describing the collection's purpose
  • is_singleton: Boolean flag determining collection type
  • created_at / updated_at: Timestamp tracking

Singleton vs Multi-Item Collections

Multi-Item Collections (is_singleton = false)

Most collections are multi-item collections, designed to hold multiple content items:

  • Blog Posts: Many blog posts in one collection
  • Products: Multiple products in an e-commerce catalog
  • Team Members: Multiple team member profiles
  • FAQ Items: Many frequently asked questions

Use multi-item collections when you need to create, manage, and query multiple instances of a content type.

Singleton Collections (is_singleton = true)

Singleton collections are designed to hold exactly one content item:

  • Homepage: Single homepage content
  • Site Settings: Global configuration
  • About Page: Single about page content
  • Footer Configuration: One footer across the site

Singleton collections are perfect for unique, one-off content that doesn't need multiple instances.

Collection Slugs and Naming

Collection slugs must be:

  • Unique within a project: No two collections can share the same slug in a project
  • URL-friendly: Use lowercase letters, numbers, and hyphens
  • Descriptive: Clearly indicate the content type ("blog-posts", not "bp")
  • Stable: Changing slugs affects API calls and queries

The combination of (project_id, slug) has a unique index, ensuring no duplicate slugs per project.

2. Field Schema Design

Field Structure

Fields are defined in the cms_collection_fields table and determine what data each content item can store. Each field has:

  • id: UUID primary key
  • collection_id: Links field to its collection
  • account_id: Account ownership
  • field_name: Identifier for the field in stored data
  • field_type: Data type (text, markdown, number, boolean, file)
  • interface_type: UI widget for editing (input, textarea, markdown, single_file, multiple_files)
  • is_required: Whether the field must have a value
  • sort_order: Display order in admin UI (lower numbers first)

Available Field Types

Vibe CMS enforces field types at the database level via check constraints:

text

Plain text content without formatting. Use for:

  • Titles and headings
  • Short descriptions
  • Names and labels
  • URLs and slugs

markdown

Rich text content with markdown formatting. Use for:

  • Blog post bodies
  • Long-form content
  • Documentation
  • Formatted descriptions

number

Numeric values (integers or decimals). Use for:

  • Prices and costs
  • Quantities and counts
  • Rankings and scores
  • Order/priority values

boolean

True/false values. Use for:

  • Feature flags (published, featured, archived)
  • Settings and toggles
  • Visibility controls

file

File references (single file or multiple files). Use for:

  • Images and media
  • Document attachments
  • Downloads and assets
  • Gallery images

Interface Types

Interface types determine how fields are edited in the admin UI:

input

Single-line text input. Best for:

  • Field types: text, number
  • Use cases: Titles, names, short text, numbers

textarea

Multi-line text input. Best for:

  • Field types: text
  • Use cases: Descriptions, summaries, longer text without formatting

markdown

Markdown editor with preview. Best for:

  • Field types: markdown
  • Use cases: Blog posts, articles, formatted content

single_file

Single file picker. Best for:

  • Field types: file
  • Use cases: Featured image, profile photo, single document
  • Storage: Stores file ID as reference

multiple_files

Multiple file picker. Best for:

  • Field types: file
  • Use cases: Image galleries, multiple attachments, media collections
  • Storage: Stores array of file IDs

Required vs Optional Fields

The is_required boolean flag determines whether a field must be populated:

  • Required fields (is_required = true): Must have a non-empty value when creating or updating content
  • Optional fields (is_required = false): Can be left empty

Required field validation is enforced by the validate_and_upsert_translation function, which checks that:

  1. All required fields are present in new translations
  2. Required fields are not empty (non-null, non-empty string)
  3. Required fields cannot be deleted in updates

Field Ordering

The sort_order integer determines the display order of fields in the admin UI:

  • Lower numbers appear first (0, 1, 2...)
  • Fields with the same sort_order are ordered by creation date
  • Use gaps (0, 10, 20) to allow easy reordering without renumbering all fields

The reorder_collection_fields function allows bulk reordering by providing a complete ordered list of field names.

Field Naming Conventions

Field names must follow these rules (enforced in application logic):

  • Lowercase only: "title", not "Title"
  • Letters, numbers, underscores: "published_at", "sort_order_2"
  • Must start with a letter: "title", not "1_title"
  • No spaces or special characters: "blog_post", not "blog-post" or "blog post"
  • Descriptive and clear: "featured_image", not "img1"

The combination of (collection_id, field_name) has a unique index.

3. Content Item Structure

Two-Table Architecture

Vibe CMS separates content metadata from translation data:

cms_content_items (Metadata)

Stores metadata about each content item:

  • id: UUID - logical identifier for the content item
  • version_id: UUID - links to cms_versions table
  • collection_id: UUID - links to cms_collections
  • project_id: UUID - project ownership
  • account_id: UUID - account ownership
  • status: text - draft, published, or archived
  • description: text - optional item description
  • Primary key: (id, version_id) - composite key for versioning

cms_content_item_data (Translation Data)

Stores locale-specific content data:

  • id: UUID - references cms_content_items.id
  • version_id: UUID - references cms_content_items.version_id
  • locale: text - language code (e.g., "en-US", "fr-FR")
  • account_id: UUID - account ownership
  • data: JSONB - flexible field storage
  • Primary key: (id, version_id, locale) - composite key

JSONB Data Storage

Content field values are stored as JSONB in the data column:

{
  "title": "Welcome to Our Blog",
  "slug": "welcome-to-our-blog",
  "content": "# Hello World\n\nThis is our first post...",
  "published_at": "2025-11-03T12:00:00Z",
  "featured_image": "file-uuid-here",
  "tags": ["announcement", "blog"]
}

JSONB provides:

  • Schema flexibility: Add/remove fields without database migrations
  • Query capability: PostgreSQL can query inside JSONB fields
  • Type preservation: Numbers, booleans, arrays stored with correct types
  • Performance: Indexed and optimized for fast queries

Translation Structure

Each content item can have multiple translations (one per locale):

Content Item (id: abc-123, version_id: v1)
├── Translation (locale: en-US)
│   └── data: { "title": "Hello", "content": "..." }
├── Translation (locale: fr-FR)
│   └── data: { "title": "Bonjour", "content": "..." }
└── Translation (locale: de-DE)
    └── data: { "title": "Hallo", "content": "..." }

Status Workflow

Content items have a status field with three states:

  1. draft: Content being created or edited (not visible publicly)
  2. published: Content ready for public viewing
  3. archived: Content no longer active but preserved

The status is enforced via check constraint:

CHECK (status = ANY (ARRAY['draft'::text, 'published'::text, 'archived'::text]))

Composite Primary Keys

Both content tables use composite primary keys for versioning support:

  • cms_content_items: (id, version_id)
  • cms_content_item_data: (id, version_id, locale)

This allows the same logical content item (same id) to exist across multiple versions, with version-specific data isolated in each version.

4. Database Architecture

Core CMS Tables

cms_collections

  • Purpose: Content type definitions
  • Primary key: id (UUID)
  • Unique constraint: (project_id, slug)
  • Key indexes: project_id, slug, account_id

cms_collection_fields

  • Purpose: Field definitions for collections
  • Primary key: id (UUID)
  • Unique constraint: (collection_id, field_name)
  • Key indexes: collection_id, (collection_id, field_name), (collection_id, sort_order)
  • Check constraints: field_type, interface_type

cms_content_items

  • Purpose: Content item metadata (draft content)
  • Primary key: (id, version_id) - composite
  • Key indexes: id, version_id, collection_id, project_id, status, (project_id, version_id)
  • Foreign keys: collection_id, version_id, project_id, account_id

cms_content_item_data

  • Purpose: Translation data for content items (draft content)
  • Primary key: (id, version_id, locale) - composite
  • Key indexes: id, version_id, locale, (id, version_id, locale)
  • Foreign keys: (id, version_id) references cms_content_items

cms_content_items_published

  • Purpose: Read-optimized table for serving published content
  • Primary key: (item_id, version_id, locale) - composite
  • Denormalized fields: collection_slug, project_id (eliminates JOINs)
  • Key indexes: (collection_slug, locale), (project_id, locale), version_id, item_id
  • Foreign keys: (item_id, version_id) references cms_content_items

Draft vs Published Architecture

Vibe CMS maintains separate tables for draft and published content:

Draft Tables (Write-Heavy)

  • cms_content_items: All content items with draft edits
  • cms_content_item_data: All translations with draft edits
  • Access: Authenticated users via SECURITY DEFINER functions
  • Use case: Content creation, editing, management

Published Table (Read-Heavy)

  • cms_content_items_published: Denormalized, read-optimized published content
  • Access: Public via get_published_content_item() and get_published_content_items_for_collection()
  • Use case: Fast public content delivery to websites and applications

Trigger-Based Synchronization

Published content is synchronized from draft tables using two strategies:

Bulk Synchronization (Primary)

Publishing operations (publish_draft, rollback_to_version) perform bulk sync:

  1. DELETE all published content for the project
  2. INSERT all status='published' items from the target version
  3. Denormalize collection_slug and project_id for fast queries

Denormalized Field Sync (Secondary)

Triggers maintain denormalized fields when source data changes:

sync_collection_slug_to_published

  • Trigger: AFTER UPDATE OF slug ON cms_collections
  • Purpose: Updates collection_slug in published table when collection slug changes
  • Scope: All published items for the affected collection

sync_locale_to_published

  • Trigger: AFTER UPDATE OF locale_code ON cms_locales
  • Purpose: Updates locale field when locale code changes
  • Scope: All published items with the affected locale

These triggers are SECURITY DEFINER and run with service_role privileges.

Security Model: Function-Only Access

All CMS tables use the "function-only access" security pattern:

  1. RLS Enabled: Row Level Security is ON for all tables
  2. No RLS Policies: No policies defined (blocks all direct queries)
  3. Table Grants: Only service_role has table-level access
  4. Function Access: All data access through SECURITY DEFINER functions
  5. Permission Checks: Functions explicitly validate has_project_access()

This provides:

  • Defense in depth: Multiple security layers
  • Auditable access: All operations through known functions
  • Clear authorization: Explicit permission checks in every function
  • Protection against SQL injection: Parameterized queries only

5. Schema Validation

Field Type Validation

Field types are enforced at multiple levels:

Database Level

Check constraints on cms_collection_fields:

CHECK (field_type = ANY (ARRAY['text', 'markdown', 'number', 'boolean', 'file']))
CHECK (interface_type = ANY (ARRAY['input', 'textarea', 'markdown', 'single_file', 'multiple_files']))

Function Level

The validate_and_upsert_translation function validates:

  • All field names in data exist in collection schema
  • All required fields are present and non-empty
  • Data structure matches expected types

Required Field Enforcement

Required fields are validated by validate_and_upsert_translation:

For new translations:

  1. Query all required fields for the collection
  2. Check each required field exists in provided data
  3. Check each required field has a non-null, non-empty value
  4. Raise exception if any required field missing or empty

For updates:

  1. Allow partial updates (only include changed fields)
  2. Prevent deletion of required fields (setting to null)
  3. Preserve existing required field values if not in update

Schema Evolution

The JSONB architecture supports schema evolution:

Adding Fields

  1. Add field to cms_collection_fields via create_collection_field()
  2. Existing content items automatically support new field
  3. New field can be required or optional
  4. Existing content remains valid (optional fields are absent)

Removing Fields

  1. Delete field from cms_collection_fields via delete_collection_field()
  2. validate_and_upsert_translation automatically cleans orphaned fields
  3. Existing field data in JSONB is cleaned on next update
  4. No data migration required

Changing Field Types

  1. Update field_type via update_collection_field()
  2. Application code handles data conversion
  3. Old data remains until updated
  4. Validation applies to new/updated content

Data Type Constraints

While JSONB is flexible, Vibe CMS enforces constraints:

  • Field existence: Only fields defined in collection schema are allowed
  • Required fields: Must be present and non-empty
  • Type consistency: Application code enforces type matching (text as string, number as number, etc.)
  • File references: File type fields store UUID references to cms_files table

6. Best Practices

When to Use Singletons vs Multi-Item Collections

Use singleton collections when:

  • Content is unique and appears once (homepage, site settings)
  • You never need multiple instances
  • Content represents global configuration
  • Examples: Homepage, About Page, Footer, Site Settings

Use multi-item collections when:

  • You need multiple instances of the same type
  • Content will be listed, filtered, or searched
  • Each item is independent
  • Examples: Blog Posts, Products, Team Members, FAQs

Field Naming Conventions

Follow these conventions for consistency:

DO:

  • Use lowercase: title, published_at
  • Use underscores for spaces: featured_image, author_name
  • Be descriptive: hero_background_image, not img1
  • Match common conventions: slug, status, created_at
  • Use consistent suffixes: _at for timestamps, _id for references

DON'T:

  • Use camelCase: featuredImage (use featured_image)
  • Use hyphens: hero-image (use hero_image)
  • Use spaces: featured image (use featured_image)
  • Use cryptic abbreviations: fbg_img (use featured_background_image)
  • Start with numbers: 2nd_title (use title_2)

Schema Design Patterns

Blog Post Collection

Collection: blog-posts (multi-item)
Fields:
  - title (text, input, required)
  - slug (text, input, required)
  - excerpt (text, textarea, optional)
  - content (markdown, markdown, required)
  - published_at (text, input, optional)
  - featured_image (file, single_file, optional)
  - author_name (text, input, required)
  - tags (text, input, optional)

Product Collection

Collection: products (multi-item)
Fields:
  - name (text, input, required)
  - slug (text, input, required)
  - description (markdown, markdown, required)
  - price (number, input, required)
  - sale_price (number, input, optional)
  - in_stock (boolean, input, required)
  - featured_image (file, single_file, required)
  - gallery_images (file, multiple_files, optional)
  - sku (text, input, required)

Homepage Collection

Collection: homepage (singleton)
Fields:
  - hero_title (text, input, required)
  - hero_subtitle (text, textarea, required)
  - hero_cta_text (text, input, required)
  - hero_cta_url (text, input, required)
  - hero_background (file, single_file, optional)
  - features_title (text, input, required)
  - features_content (markdown, markdown, required)

Performance Considerations

Indexing Strategy

  • Collection slugs: Indexed for fast lookup by slug
  • Field names: Composite index (collection_id, field_name) for validation
  • Content queries: Indexed by collection_id, version_id, locale
  • Published content: Denormalized and indexed for public API performance

JSONB Query Performance

  • JSONB supports GIN indexes for fast field queries
  • Denormalized published table eliminates JOINs
  • Composite primary keys enable efficient version isolation
  • Status index accelerates published content filtering

Scaling Recommendations

  • Use published table for all public content delivery
  • Cache frequently accessed content at application layer
  • Minimize field count (aim for < 20 fields per collection)
  • Use file references instead of embedding large data
  • Consider content pagination for large collections

Content Modeling Strategies

Keep Collections Focused

  • One collection per distinct content type
  • Avoid "mega collections" with dozens of fields
  • Split complex content into multiple collections with relationships

Plan for Localization

  • All content is locale-specific by default
  • Think about which fields need translation vs which are universal
  • File references are locale-agnostic (same file across locales)

Design for Editors

  • Order fields logically (title first, metadata last)
  • Use required fields sparingly (only truly required)
  • Provide clear field names and descriptions
  • Choose appropriate interface types for data entry

Future-Proof Your Schema

  • Add optional fields for future needs
  • Use consistent naming across collections
  • Document field purposes and expected values
  • Plan for schema evolution (JSONB makes this easy)

What's Next?

Now that you understand Vibe CMS's content architecture, you're ready to explore:

  • Chapter 5: Files & Assets - Managing media, images, and file uploads
  • Chapter 6: Versioning & Publishing - Content versioning, rollback, and publishing workflows
  • Chapter 7: SDK Integration - Using the TypeScript SDK to query content in your application

This chapter covers the database architecture and content modeling patterns in Vibe CMS. For API reference and code examples, see the SDK documentation.