---
name: model-kinds
description: "Detailed model kinds reference including storage formats, relations, nesting rules, and examples for all four kinds."
---

# Model Kinds Reference

Contentrain has four model kinds, each with distinct storage format, ID management, and use cases.

## Singleton

**One instance per locale.** Use for page sections, site config, navigation.

| Aspect | Detail |
|--------|--------|
| Storage | JSON object (one file per locale) |
| File path | `.contentrain/content/{domain}/{model-id}/{locale}.json` |
| `i18n: false` path | `.contentrain/content/{domain}/{model-id}/data.json` |
| ID management | None (single instance) |
| Relation target | Cannot be referenced by relations |

### Example Model

```json
{
  "id": "hero-section",
  "name": "Hero Section",
  "kind": "singleton",
  "domain": "marketing",
  "i18n": true,
  "title_field": "title",
  "fields": {
    "title": { "type": "string", "required": true, "max": 80 },
    "subtitle": { "type": "text", "max": 200 },
    "cta": { "type": "string", "required": true },
    "cta_url": { "type": "url", "required": true },
    "background": { "type": "image" }
  }
}
```

### Example Content (`en.json`)

```json
{
  "background": "assets/hero-bg.webp",
  "cta": "Get Started",
  "cta_url": "https://app.example.com/signup",
  "subtitle": "The modern content platform for developers",
  "title": "Build faster with Contentrain"
}
```

**Use for:** Hero sections, navigation menus, site settings, form labels, footer content -- anything with exactly one instance per locale.

## Collection

**Multiple entries.** Use for team members, products, FAQs, categories.

| Aspect | Detail |
|--------|--------|
| Storage | JSON object-map (entry ID as key, sorted lexicographically) |
| File path | `.contentrain/content/{domain}/{model-id}/{locale}.json` |
| Tool output | Array with `id` injected |
| ID management | Auto-generated 12-char hex |
| Relation target | Referenced by entry `id` |

### Example Model

```json
{
  "id": "team-members",
  "name": "Team Members",
  "kind": "collection",
  "domain": "marketing",
  "i18n": true,
  "title_field": "name",
  "fields": {
    "name": { "type": "string", "required": true },
    "role": { "type": "string", "required": true },
    "avatar": { "type": "image" },
    "bio": { "type": "text" },
    "linkedin": { "type": "url" }
  }
}
```

### Storage Format (on disk)

```json
{
  "a1b2c3d4e5f6": {
    "avatar": "assets/ahmet.jpg",
    "bio": "Founder and CEO",
    "linkedin": "https://linkedin.com/in/ahmet",
    "name": "Ahmet",
    "role": "CEO"
  },
  "f6e5d4c3b2a1": {
    "avatar": "assets/jane.jpg",
    "bio": "Technical leader",
    "linkedin": "https://linkedin.com/in/jane",
    "name": "Jane",
    "role": "CTO"
  }
}
```

### MCP Tool Output (differs from storage)

MCP tools return collections as arrays with `id` injected:

```json
[
  { "id": "a1b2c3d4e5f6", "name": "Ahmet", "role": "CEO", "avatar": "assets/ahmet.jpg" },
  { "id": "f6e5d4c3b2a1", "name": "Jane", "role": "CTO", "avatar": "assets/jane.jpg" }
]
```

### ID Management

- IDs are 12-char hex strings, auto-generated by MCP tools
- Provide `id` in `contentrain_content_save` entry to update an existing entry
- Omit `id` to create a new entry (ID auto-generated)
- The same ID is used across all locale files for the same entry

**Why object-map?**
- Sorted keys produce predictable diffs and minimize Git merge conflicts
- No ID duplication -- key IS the ID
- Two branches adding different entries rarely conflict (~0.3% chance for 350 entries)

## Document

**Markdown with frontmatter.** Use for blog posts, documentation, changelogs.

| Aspect | Detail |
|--------|--------|
| Storage | `.md` file with YAML frontmatter |
| File path | `.contentrain/content/{domain}/{model-id}/{slug}/{locale}.md` |
| `i18n: false` path | `.contentrain/content/{domain}/{model-id}/{slug}.md` |
| ID management | `slug` field (URL-safe, unique) |
| Relation target | Referenced by `slug` |

### Example Model

```json
{
  "id": "blog-post",
  "name": "Blog Post",
  "kind": "document",
  "domain": "blog",
  "i18n": true,
  "title_field": "title",
  "fields": {
    "title": { "type": "string", "required": true, "max": 120 },
    "slug": { "type": "slug", "required": true, "unique": true },
    "excerpt": { "type": "text", "max": 200 },
    "author": { "type": "relation", "model": "team-members", "required": true },
    "tags": { "type": "array", "items": "string" },
    "cover": { "type": "image" }
  }
}
```

### Example Content (`getting-started/en.md`)

```markdown
---
title: Getting Started
slug: getting-started
excerpt: Learn how to set up Contentrain in your project
author: a1b2c3d4e5f6
tags: [tutorial, intro]
cover: assets/getting-started-cover.webp
---
# Getting Started with Contentrain

Your markdown body content here...
```

### Slug Management

- Slug is the directory name and identity for the document
- Provide `slug` in `contentrain_content_save` entry (required for documents)
- Slug must be URL-safe: lowercase, kebab-case, matching `/^[a-z0-9]+(?:-[a-z0-9]+)*$/`
- Same slug used across all locale files

**Use for:** Blog posts, documentation pages, changelogs, knowledge base articles -- long-form content with typed metadata.

## Dictionary

**Flat key-value pairs.** Use for error messages, UI strings, translations.

| Aspect | Detail |
|--------|--------|
| Storage | Flat JSON object (key-value, all values are strings) |
| File path | `.contentrain/content/{domain}/{model-id}/{locale}.json` |
| Fields definition | None -- dictionary models have NO `fields` property |
| ID management | Key is identity |
| Relation target | Cannot be referenced by relations |

### Example Model

```json
{
  "id": "error-messages",
  "name": "Error Messages",
  "kind": "dictionary",
  "domain": "system",
  "i18n": true,
  "title_field": "key"
}
```

### Example Content (`en.json`)

```json
{
  "auth.expired": "Session expired. Please log in again.",
  "auth.failed": "Authentication failed. Check your credentials.",
  "auth.locked": "Account locked. Contact support.",
  "validation.email": "Please enter a valid email address.",
  "validation.required": "{field} is required."
}
```

### Key Naming Rules

- Semantic, hierarchical, dot-separated keys
- Always in English regardless of locale
- Lowercase with dots as separators, no spaces, no camelCase
- Group by feature/domain, not by page/position
- GOOD: `auth.login.button`, `errors.404.title`, `nav.main.dashboard`
- BAD: `button1`, `text_3`, `label_top`, `str_47`

**Use for:** Error messages, UI strings, i18n translation files, mobile app strings, form validation messages.

## Relations

### Basic Relations

```json
"author": { "type": "relation", "model": "team-members", "required": true }
"categories": { "type": "relations", "model": "categories" }
```

- `relation` (1:1): Value is a single string -- an entry ID or slug
- `relations` (1:many): Value is a string array of entry IDs or slugs

### Target Model Restrictions

| Target Kind | Reference Key | Can Be Target? |
|-------------|--------------|----------------|
| Collection | Entry `id` | Yes |
| Document | Document `slug` | Yes |
| Singleton | -- | No |
| Dictionary | -- | No |

### Polymorphic Relations

When a field can reference multiple model types:

```json
"target": { "type": "relation", "model": ["blog-post", "page"] }
```

Storage for polymorphic references uses a compound value:

```json
{ "model": "blog-post", "ref": "getting-started" }
```

### Self-Referencing

Models can reference themselves (e.g., hierarchical categories):

```json
"parent": { "type": "relation", "model": "categories" }
```

### Resolution Rules

- Relations are resolved **1 level deep** -- no recursive resolution
- **Unresolved IDs** (target entry does not exist) are kept as raw strings (graceful degradation)
- **Cascade deletion does not exist.** Deleting a referenced entry produces a broken relation warning
- **Array order is preserved** for `relations` type
- **IDs/slugs are locale-agnostic.** The same reference works across all locales

### Validation

- Referenced ID/slug MUST exist in the target model
- `model` property MUST reference an existing model ID
- Referential integrity is checked by `contentrain_validate`
- Deleting a model that is referenced by other models is BLOCKED

## Nesting

### Object Type

```json
"address": {
  "type": "object",
  "fields": {
    "city": { "type": "string", "required": true },
    "street": { "type": "string", "required": true },
    "zip": { "type": "string" }
  }
}
```

### Array of Objects

```json
"variants": {
  "type": "array",
  "items": {
    "type": "object",
    "fields": {
      "color": { "type": "color", "required": true },
      "price": { "type": "decimal", "required": true },
      "size": { "type": "select", "options": ["S", "M", "L"] }
    }
  },
  "max": 50
}
```

### Depth Limit

**Maximum nesting depth: 2 levels.** An object inside an object is allowed. An object inside an object inside an object is NOT.

- Prefer flat types over deeply nested structures
- Use relations to model complex data relationships instead of nesting
- If you need deeper nesting, create a separate model and use a `relation` field
