CMS Data Models
Typed, localized, publishable data models and inline variables for CMS content.
Overview
Yayaw separates CMS content data from design tokens.
/dashboard/content/design-tokensremains the design-token editor./dashboard/content/data-modelsmanages scoped CMS data model structure./dashboard/content/datafills and publishes entries for those models.
CMS data models can be scoped:
global: shared site-level data controlled byglobal-data:manage.organization: active-organization data controlled bypage:manageorsections:manageon that organization.
Media-like values should be stored as URLs or strings. The organization media library remains scoped separately. Use Media Library for organization-owned binary assets and store only the resulting URLs in global or organization data entries.
CMS Role
CMS data is the structured source layer for content that needs a stable contract: site variables, menus, product facts, organization facts, reusable collections, and fields that pages or sections bind by reference. Data entries should hold values, not presentation. Sections and pages decide how those values render.
Use data models when authors need typed fields, localization, validation, and a published lifecycle. Use media assets for files, sections for reusable visual composition, and design tokens for visual theme values.
Document Contracts
Global data uses two versioned JSON documents:
GlobalDataModelDocumentV1GlobalDataEntryDocumentV1
Model documents define:
cardinality:singletonorcollectionfields: typed fields inspired by Builder custom fieldslocalization: default locale and supported localespresentation:headless,builtin_renderer, orcomponent_recipe
Cardinality controls how many entries a model can own:
singleton: one global entry, for exampleheader-menu,footer-menu, or default SEO settings.collection: many entries, for example products, authors, redirects, or reusable announcements.
Presentation controls how data is consumed:
headless: structured data only, consumed by pages, bindings, helpers, or custom code.builtin_renderer: data rendered by Yayaw runtime components, currently used by first-party models such as header and footer.component_recipe: data intended to be paired with a reusable UI recipe in a later workflow.
Each field supports the Builder-style settings editors expect:
- type
- localization
- default value
- helper text
- required/optional
- enum values for select fields
- hidden fields that stay out of entry editing while preserving values/defaults
Hidden required fields must define a default value so generated entry editors cannot create impossible drafts.
Supported V1 field types are:
text,long_text,url,filenumber,boolean,select,colorrich_text,html,date,timestamplist,reference,map,javascript,code,tags,json
file stores a URL/string in V1 because the media library is organization-scoped
while global data is site-wide.
Entry documents store values as either:
shared: one value for every localelocalized: values per locale with fallback from requested locale to default locale, then first available locale
Storage
CMS data uses immutable revisions and one publication row per registry item.
Tables:
global_data_model_registry_itemsglobal_data_model_revisionsglobal_data_model_publicationsglobal_data_entry_registry_itemsglobal_data_entry_revisionsglobal_data_entry_publications
Revisions are hash-deduped and validated before publish. Publications can be
draft, published, or archived.
global_data_model_registry_items stores scope and organization_id.
Existing rows are migrated to scope = "global". Slugs are unique per global
scope or per organization scope.
Dashboard Editing
The dashboard provides:
- model list using Yayaw Table
- full-width model field editor
- dedicated entry list on
/dashboard/content/data - generated inspector-style entry editor based on the selected model fields
- save, publish, and archive actions
- read-only rendering for users without manage permission
Editing requires:
global-data:managefor global models.page:manageorsections:manageon the active organization for organization models.
Read-only access is available through:
global-data:readorglobal-data:listfor global models.page:read,page:list,sections:read, orsections:listfor organization models.
Runtime APIs
Server helpers live in:
src/lib/server/services/global-data
Key helpers:
getPublishedGlobalDataEntrygetPublishedGlobalDataValuelistPublishedGlobalDataEntrieslistPublishedGlobalDataFieldReferences
Most helpers accept scope and organizationId when organization-scoped data is
needed. Existing page-builder global_data_field and global_data_query
bindings continue to resolve global data.
The shared runtime slot component lives in:
src/blocks/shared/global-data-slot.tsx
It resolves a published singleton entry and falls back to the existing static UI when no published data is available.
Header and Footer
Header and footer are no longer seeded section catalog entries or editable page props on every page.
The seed script creates two singleton built-in renderer models:
header-menufooter-menu
Both receive a default main entry.
Generated page headers and footers bind to these singleton entries with
global_data_field props. Pages therefore share one localized source of truth
for brand labels, locale-specific brand URLs, menu items, footer tagline, and
copyright text.
The items_json field is edited with a visual menu editor in the global data
entry form on /dashboard/content/data. It supports top-level links, groups
with one nested link level, theme toggles, language toggles, account actions,
reorder controls, and a raw JSON recovery panel.
Header items can target the left, center, or right zones; the default entry uses
the left brand link, center navigation, and right-side theme/language/account
actions.
The default header-menu/main and footer-menu/main entries organize public
navigation into two first-level groups:
Yayaw:/[locale]/codebase,/[locale]/pricing, and/[locale]/docsYayaw Table:/[locale]/table,/[locale]/table/example,/[locale]/docs/table, and/[locale]/docs/table/installation
Keep Yayaw Table developer documentation links under the Fumadocs subproject at
/[locale]/docs/table/*. CMS-authored /[locale]/table/* pages remain the
public product and demo surfaces.
Old site-header, site-footer, builtin-header, and builtin-footer seed
rows are deleted by migration. Legacy page documents that still reference the
old built-in sections are normalized into generated header/footer nodes.
Page Builder Bindings
Page prop bindings support global data in addition to literals and media assets.
Binding kinds:
global_data_field: binds one field from one published entryglobal_data_query: resolves a published collection and optionally extracts one field
The Puck inspector exposes a "Bind data" control when published global data references are available. V1 binds complete fields.
Inline CMS variables can also be inserted into text values from the dashboard
header catalog on /dashboard/content/* routes. Tokens are namespaced to avoid
collisions:
{site.name}{organization.logo}{data.global.header-menu.main.brand_label}{data.organization.<modelSlug>.<entrySlug>.<fieldKey>}{billing.product.pro_monthly.name}{billing.price.pro_monthly.id}
Unknown tokens remain unchanged at runtime. Non-namespaced tokens such as
{name} are ignored in V1.
Site variables are edited through the CMS Data entry for the built-in
site-variables/main singleton model. Its default fields are locked so the
editing surface can change values without deleting the canonical site.*
contract. Each field still keeps its own localization setting: technical values
such as base_url, domain, and logo URLs stay shared, while editorial values
such as description and title can be localized and resolve according to the
requested locale. Existing system_settings values under
cms.site-variables.v1 remain as migration and fallback data.
Billing product and price variables are read from the runtime billing product
catalog. Product metadata comes from billing_products when configured, while
Stripe price IDs are stored internally after the admin or MCP catalog sync.
Organization variables are read from the Better Auth organization row; editing
continues to live in organization settings flows.
The built-in billing-product-content global collection complements the billing
catalog with one CMS entry per product key. Its locked catalog_product_key
field links the CMS entry to Products & Services; editorial fields such as
badge, summary, feature bullets, CTA label, and highlight state can be localized.
Do not store product names, prices, intervals, Stripe IDs, or checkout
availability in this model. Resolve those facts from the billing.product.* and
billing.price.* variable namespaces.
The seed also carries the Supabase-exported global data snapshot for production
site variables, header and footer menus, and the sales-offer/main singleton.
These entries are published on every seed run so fresh production or preview
databases resolve the same CMS variables that were authored in Supabase. The
billing-product-content entries are still created only when missing so later
editorial product copy is not overwritten by an empty baseline seed.
Cache Invalidation
Published global data is cached with tags:
global-dataglobal-data:{modelSlug}
Publish and archive actions revalidate the global data tags, dashboard data routes, the public root path, and the published page runtime cache.
Validation
Useful checks after CMS data model changes:
bun test src/lib/shared/global-data/global-data.test.ts
bun run check
bunx tsc --noEmit