raisindb-content-modeling
RaisinDB Content Modeling
Define content schemas, page templates, and composable blocks using YAML files inside a RaisinDB package.
MANDATORY: After creating or modifying ANY .yaml or .node.yaml file in package/, immediately run:
npm run validate
Fix all errors before proceeding. Never skip validation.
1. NodeType YAML
NodeTypes define the data schema for content. Place files in package/nodetypes/.
Naming
Use namespace:PascalCase format: myapp:Article, launchpad:Page, shop:Product.
Full Example
name: launchpad:Page
title: Page
description: Base page type for Launchpad content
icon: file-text
color: "#6366f1"
version: 1
properties:
- name: title
title: Title
type: String
required: true
is_translatable: true
index:
- Fulltext
- name: slug
title: Slug
type: String
required: true
- name: description
title: Description
type: String
required: false
is_translatable: true
versionable: true
publishable: true
auditable: true
indexable: true
Property Types
| Type | Stored as | Example |
|---|---|---|
String |
JSON string | "hello" |
Number |
JSON number | 42 |
Boolean |
JSON boolean | true |
Date |
ISO 8601 string | "2025-01-15" |
Object |
JSON object | {"key": "val"} |
Array |
JSON array | ["a", "b"] |
Reference |
raisin:ref object |
See below |
Resource |
Resource metadata | File attachment |
Property Options
| Option | Type | Description |
|---|---|---|
required |
boolean | Must be set on creation |
is_translatable |
boolean | Enable i18n translation |
default |
any | Default value when not provided |
index |
string[] | Index types, e.g. [Fulltext] |
Top-Level Flags
| Flag | Effect |
|---|---|
versionable |
Tracks revision history, enables draft/publish workflow |
publishable |
Adds publish/unpublish lifecycle |
auditable |
Records who changed what and when |
indexable |
Includes in full-text search index |
Reference Values at Runtime
A Reference property stores a raisin:ref object: {"raisin:ref": "node-id-or-path", "raisin:workspace": "workspace-name"}.
2. Archetype YAML
Archetypes define page templates linking a NodeType to editor fields. Place files in package/archetypes/. Multiple archetypes can share one NodeType for different layouts.
Example
name: launchpad:LandingPage
title: Landing Page
description: Landing page template with hero, content blocks, and features
icon: layout
color: "#6366f1"
base_node_type: launchpad:Page
version: 1
fields:
- $type: TextField
name: title
title: Page Title
required: true
translatable: true
- $type: TextField
name: slug
title: URL Slug
required: true
- $type: SectionField
name: content
title: Page Content
allowed_element_types:
- launchpad:Hero
- launchpad:TextBlock
- launchpad:FeatureGrid
publishable: true
Field Types
| Field Type | Purpose |
|---|---|
TextField |
Text input (single or multi-line) |
NumberField |
Numeric input |
BooleanField |
Toggle / checkbox |
DateField |
Date or datetime picker |
MediaField |
File/image upload (maps to Resource property) |
RichTextField |
Rich text / HTML editor |
SectionField |
Container for ElementTypes -- the composition mechanism |
CompositeField |
Group of sub-fields; use multiple: true for repeatable lists |
ReferenceField |
Link to another RaisinDB node (raisin:ref object). Use for CTA links, related content, author, tags. Use multiple: true for arrays of references |
Field Options
All fields support: name, title, required, translatable, description, multiple.
| Option | Type | Description |
|---|---|---|
name |
string | Unique field identifier (required) |
title |
string | Human-readable label for the editor UI |
required |
boolean | Field must have a value |
translatable |
boolean | Field supports translation overlays |
description |
string | Help text shown in the editor |
multiple |
boolean | Allow multiple values (stores as array). Works on any field type. On CompositeField, creates a repeatable list of structured items |
SectionField -- Composition Mechanism
SectionField composes pages from ElementTypes. Declare which element types are allowed via allowed_element_types. At runtime the property stores an array of element objects.
CompositeField -- Repeatable Sub-Fields
Use CompositeField with multiple: true for repeatable arrays of structured objects. Any field type supports multiple: true, but on CompositeField it creates a list of structured items. You can nest SectionField inside a CompositeField:
- $type: CompositeField
name: columns
title: Columns
multiple: true
translatable: true
fields:
- $type: TextField
name: id
title: Column ID
required: true
- $type: TextField
name: title
title: Column Title
required: true
translatable: true
- $type: SectionField
name: cards
title: Cards
translatable: true
allowed_element_types:
- launchpad:KanbanCard
UUID Rule for Repeatable CompositeFields
When a repeatable CompositeField has ANY sub-field with translatable: true, each item in the content YAML must have a unique uuid. This enables per-item translation merging without losing non-translatable fields.
# In content YAML — items need uuid when composite has translatable fields
features:
- uuid: feat-1
title: Fast Development # translatable
icon: zap # NOT translatable — preserved during translation
- uuid: feat-2
title: Scalable
icon: trending-up
Without UUIDs, npm run validate will emit COMPOSITE_MISSING_UUID. Duplicate UUIDs within the same array emit COMPOSITE_DUPLICATE_UUID.
ReferenceField — Node References
Use ReferenceField for any link to another RaisinDB node: CTA buttons linking to pages, related articles, author references, tag links, parent categories. Do NOT use TextField for these — TextField is only for external URLs (https://...), email addresses, or arbitrary strings that don't point to RaisinDB nodes.
Schema definition:
fields:
- $type: ReferenceField
name: cta_link
title: CTA Link
config:
allowed_entry_types:
- launchpad:Page
- $type: ReferenceField
name: author
title: Author
required: true
config:
allowed_entry_types:
- myapp:Author
config.allowed_entry_types restricts which node types can be referenced. Omit it to allow any node type.
Content YAML format — both raisin:ref and raisin:workspace are required:
properties:
cta_link:
raisin:ref: /launchpad/contact
raisin:workspace: launchpad
author:
raisin:ref: /launchpad/authors/jane
raisin:workspace: launchpad
raisin:ref— node path (starting with/) or node ID (UUID). Paths are auto-resolved to UUIDs during INSERT/UPDATE.raisin:workspace— target workspace name. Required even for same-workspace references.- Referenced node must exist at INSERT/UPDATE time (path-based refs are validated).
Frontend usage — read raisin:path for links:
<a href={element.cta_link?.['raisin:path']}>{element.cta_text}</a>
Multiple References
Use ReferenceField with multiple: true for one-to-many node references:
fields:
- $type: ReferenceField
name: related_articles
title: Related Articles
multiple: true
config:
allowed_entry_types:
- myapp:Article
Content YAML stores an array of references:
properties:
related_articles:
- raisin:ref: /blog/articles/intro-to-raisin
raisin:workspace: blog
- raisin:ref: /blog/articles/advanced-queries
raisin:workspace: blog
Querying References
-- Find all nodes that reference a specific target
SELECT * FROM blog
WHERE REFERENCES('blog:/articles/intro-to-raisin')
-- Resolve references inline (replace raisin:ref objects with full node data)
SELECT RESOLVE(properties) FROM blog WHERE path = $1
-- Resolve with depth control (nested references, max 10)
SELECT RESOLVE(properties, 3) FROM blog WHERE path = $1
3. ElementType YAML
ElementTypes are composable content blocks placed inside SectionField containers. Place files in package/elementtypes/. They use the same field types as archetypes.
Hero Example
name: launchpad:Hero
title: Hero Section
description: Full-width hero section with headline, subheadline, and call-to-action
icon: image
color: "#8b5cf6"
version: 1
fields:
- $type: TextField
name: headline
title: Headline
required: true
translatable: true
- $type: TextField
name: subheadline
title: Subheadline
required: false
translatable: true
- $type: TextField
name: cta_text
title: CTA Button Text
required: false
translatable: true
- $type: ReferenceField
name: cta_link
title: CTA Button Link
required: false
config:
allowed_entry_types:
- launchpad:Page
- $type: MediaField
name: background_image
title: Background Image
required: false
RichTextField Example
Use RichTextField for rich text / HTML content:
- $type: RichTextField
name: content
title: Content
required: true
translatable: true
ElementType with Nested CompositeField
ElementTypes can use CompositeField with multiple: true for repeatable arrays of structured items:
name: launchpad:FeatureGrid
title: Feature Grid
icon: grid-3x3
color: "#f59e0b"
version: 1
fields:
- $type: TextField
name: heading
title: Section Heading
translatable: true
- $type: CompositeField
name: features
title: Features
multiple: true
fields:
- $type: TextField
name: title
title: Feature Title
required: true
translatable: true
- $type: TextField
name: description
title: Feature Description
required: true
translatable: true
Dynamic Listing Pattern
Instead of hardcoding items in a CompositeField array, model a dynamic listing element type that references a folder and queries children at render time. This is content-driven: editors configure what to show, the frontend queries for it.
Element type (elementtypes/content-listing.yaml):
name: myapp:ContentListing
title: Content Listing
description: Dynamically lists child nodes of a referenced folder
icon: list
color: "#3b82f6"
version: 1
fields:
- $type: TextField
name: heading
title: Section Heading
translatable: true
- $type: ReferenceField
name: source
title: Source Folder
required: true
config:
allowed_entry_types:
- raisin:Folder
- $type: TextField
name: filter_node_type
title: Filter by Node Type
description: "e.g. myapp:Project"
- $type: NumberField
name: limit
title: Max Items
- $type: TextField
name: sort_by
title: Sort By
description: "Property name to sort by, e.g. title, created_at"
Content YAML — just configure, no data duplication:
content:
- uuid: projects-listing-1
element_type: myapp:ContentListing
heading: Featured Projects
source:
raisin:ref: /myapp/projects
raisin:workspace: myapp
filter_node_type: myapp:Project
limit: 6
sort_by: created_at
Frontend component — queries at render time:
<script lang="ts">
export let element: ContentListingElement
const sourcePath = element.source?.['raisin:path']
const nodeType = element.filter_node_type || ''
const limit = element.limit || 10
// Use CHILD_OF for direct children only, or DESCENDANT_OF for
// the full subtree (nested folders like /projects/2025/01/post).
// Results are already in natural path order (no ORDER BY needed).
const sql = `
SELECT id, path, name, properties
FROM ${WORKSPACE}
WHERE DESCENDANT_OF('${sourcePath}')
${nodeType ? `AND node_type = '${nodeType}'` : ''}
LIMIT ${limit}
`
const items = query(sql)
</script>
<section>
<h2>{element.heading}</h2>
{#each items as item}
<a href={item.path}>{item.properties.title}</a>
{/each}
</section>
Choose based on your content structure:
CHILD_OF('/projects')— direct children only (flat list)DESCENDANT_OF('/projects')— full subtree, handles nested folders like/projects/2025/01/my-project
Results are returned in natural path order by default — no ORDER BY needed. Add ORDER BY only when sorting by a different field (e.g., ORDER BY properties->>'created_at'::String DESC).
This pattern avoids duplicating content data in composite arrays. Editors add/remove/reorder items by managing actual nodes in the tree, and any listing element pointing to that folder updates automatically.
Runtime Representation
Elements appear in properties.content[] with uuid and element_type:
{ "uuid": "hero-1", "element_type": "launchpad:Hero", "headline": "Launch Your Vision" }
4. Mixins
Mixins are reusable property sets. Define them as NodeTypes with is_mixin: true. Place files in package/mixins/.
name: myapp:SEOFields
title: SEO Fields
description: Common SEO properties for pages
is_mixin: true
properties:
- name: meta_title
title: Meta Title
type: String
- name: meta_description
title: Meta Description
type: String
- name: og_image
title: Open Graph Image
type: Resource
Register under provides.mixins in manifest.yaml. Mixins install before NodeTypes.
5. Workspace YAML
Workspaces scope content and control allowed NodeTypes. Place files in package/workspaces/.
name: launchpad
title: Launchpad
description: Content workspace for Launchpad portal
icon: rocket
color: "#6366f1"
allowed_node_types:
- launchpad:Page
- raisin:Folder
- raisin:Asset # Required for file uploads
allowed_root_node_types:
- raisin:Folder
- launchpad:Page
root_structure:
- name: pages
node_type: raisin:Folder
title: Pages
description: Site pages
allowed_node_types-- types that can be created in this workspaceallowed_root_node_types-- types allowed at root levelroot_structure-- nodes created automatically on workspace init
6. manifest.yaml
Declares package metadata and all provided types. Place at package/manifest.yaml.
name: launchpad-next
version: 1.0.5
title: Launchpad Next
description: LaunchKit customer portal
author: SOLUTAS GmbH
provides:
nodetypes:
- launchpad:Page
archetypes:
- launchpad:LandingPage
- launchpad:KanbanBoard
- launchpad:FileBrowser
elementtypes:
- launchpad:Hero
- launchpad:TextBlock
- launchpad:FeatureGrid
- launchpad:KanbanCard
workspaces:
- launchpad
functions:
- /lib/launchpad/handle-read-receipt
triggers:
- /triggers/on-read-receipt
workspace_patches:
launchpad:
allowed_node_types:
add:
- launchpad:Page
- raisin:Folder
- raisin:Asset # Required for file uploads
Supported provides keys: nodetypes, archetypes, elementtypes, mixins, workspaces, functions, triggers, flows. Each maps to its directory (nodetypes/, archetypes/, etc.; functions use content/functions/lib/, triggers use content/functions/triggers/).
Use workspace_patches to add allowed node types to existing workspaces without overwriting their definition. Always include raisin:Asset if the workspace needs file uploads — without it, uploads will fail silently.
7. Content YAML
Content files define initial data installed with the package. Place .node.yaml files inside package/content/{workspace}/{path}/. The directory structure maps to the node tree.
File: package/content/launchpad/launchpad/home/.node.yaml
node_type: launchpad:Page
archetype: launchpad:LandingPage
properties:
title: Welcome to Launchpad
slug: home
description: Your gateway to launching amazing projects
content:
- uuid: hero-1
element_type: launchpad:Hero
headline: Launch Your Vision
subheadline: Build, deploy, and scale your ideas with Launchpad
cta_text: Get Started
cta_link:
raisin:ref: /launchpad/contact
raisin:workspace: launchpad
- uuid: intro-1
element_type: launchpad:TextBlock
heading: Why Launchpad?
content: |
Launchpad is your all-in-one platform for turning ideas into reality.
- uuid: features-1
element_type: launchpad:FeatureGrid
heading: Features
features:
- uuid: feat-fast
icon: zap
title: Fast Development
description: Build and iterate quickly with our modern stack
Content YAML Rules
node_type-- required, must match a type inprovides.nodetypesarchetype-- optional, links to an archetype for the editor UIproperties-- the node's property values- Each element in
contentneeds a uniqueuuidand a validelement_type - Element fields correspond to the ElementType's field definitions
8. Validation
MANDATORY — run after every YAML change:
npm run validate
Do NOT proceed until all errors are resolved.
Common Errors
| Error | Cause |
|---|---|
INVALID_NODE_TYPE_NAME |
Use namespace:PascalCase format (e.g. myapp:Article) |
MISSING_REQUIRED_FIELD |
Manifest needs name and version at minimum |
UNKNOWN_NODE_TYPE_REFERENCE |
Referenced type not listed in provides.nodetypes |
DUPLICATE_PROPERTY |
No duplicate property names within a single NodeType |
UNKNOWN_ELEMENT_TYPE |
Element type in allowed_element_types not in provides.elementtypes |
MISSING_BASE_NODE_TYPE |
Archetype base_node_type not found in provides.nodetypes |
COMPOSITE_MISSING_UUID |
Repeatable composite with translatable sub-fields has items without uuid |
COMPOSITE_DUPLICATE_UUID |
Two items in the same composite array share the same uuid |
Every type in provides must have a corresponding YAML file in the matching directory (nodetypes/, archetypes/, elementtypes/, mixins/, workspaces/). File names use kebab-case (e.g., landing-page.yaml for launchpad:LandingPage).
More from maravilla-labs/raisindb
raisindb-frontend-react
Build a React Router frontend for your RaisinDB app with path-based routing, archetype-to-component mapping, and SSR-to-WebSocket upgrade. Use when creating a React frontend.
3raisindb-overview
Core concepts of RaisinDB content-driven applications. Use when building any RaisinDB app. Teaches: path-as-URL routing, archetype-to-component mapping, content modeling, project structure.
3raisindb-access-control
Roles, permissions, groups, and row-level security for RaisinDB. Configure anonymous access, custom roles, and fine-grained permissions in your package. Use when setting up authorization.
3raisindb-frontend-sveltekit
Build a SvelteKit frontend for your RaisinDB app with path-based routing, archetype-to-component mapping, and real-time updates. Use when creating a SvelteKit frontend.
3raisindb-functions-triggers
Server-side JavaScript functions and event-driven triggers for RaisinDB. Covers function definitions, the raisin.* runtime API, transactions, trigger filters, and event handling. Use when adding server-side logic.
3