> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stackra.figentra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 0116 — @stackra/content: content types are models with a trait bundle; custom fields are a free, bounded trait

> Accepted · 2026-10-19

## Context

Pages, forms, posts and many other authored things share drafts, versions, publishing, slugs, SEO, translation, permissions
and search. One package per type would repeat all of that. A generic JSON store for every type would be the
"JSONB-for-everything" that [0011](/adr/0011-rejected-and-deferred-technologies) rejects. Tenants also want extra fields on
both content and business records (a Student, an Order).

## Decision

1. **CT1 — `@stackra/content` is the free content core.** Page, form definition and post are built-in content types sharing
   drafts, versions, publishing, slugs, SEO, i18n, permissions, search and the document format of
   [0110](/adr/0110-experience-is-the-sdui-service-at-l2). New types are declared in code with `@ContentType` in any module.

2. **CT3 — `@ContentType` is `@Model` plus a trait bundle**: `HasRevisions`, `Publishable`, `Sluggable`, `HasSeo`,
   `Translatable`, `Searchable`, `HasCustomFields` and routes ([0096](/adr/0096-trait-naming-grammar)). Each type is a real,
   typed table generated by gen ([0097](/adr/0097-model-and-database-packages)); there is no generic JSON store.

3. **The decision rule**:

   | The thing is | Model it as |
   | - | - |
   | Authored and published, with no business rules | A content type |
   | Bound by invariants, money or a state machine | A domain `@Model` |
   | Both | A domain model plus the content traits it needs |

4. **CT4 — Built-in content types**:

   * page, post, form definition, gallery;
   * help article, FAQ, announcement, changelog;
   * legal document (acceptance feeds consent), testimonial, team member, job posting;
   * reusable blocks, remote navigation menus (which feed `@stackra/navigation`), redirects and short links.

   Submissions, products, courses, orders and bookings are domain models, not content.

5. **CT2 — `HasCustomFields` is free and bounded**, on content types and on business models. Values live in a side table and
   are validated by the stored field schema; how many fields a tenant may add is a plan limit enforced through entitlements.

6. **Tenant-defined new content types at runtime are Pro and later**, and need their own amendment of 0011 first.

## Consequences

* Every content type gets publishing, SEO and search for free, and stays a typed table that migrations and RLS understand.
* The decision rule gives reviewers a test for "should this be content".
* Custom fields add a side-table join to reads; search and list filtering must learn about them.

## Enforcement

Planned: the gen validation pass fails a `@ContentType` that also declares a state machine or money field without the
domain-model form, and a `HasCustomFields` model with no entitlement limit key. Nothing checks this today.

## Supersedes and sources

Supersedes: none. Sources: owner decisions in chat, 2026-10-19 (Round 11 CT1, CT2; "LOCKED content/workflow studio" CT3, CT4).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.