RaisFastRaisFast
Content Types

Protocols

11 composable behaviors that add columns and logic to content types without writing code.

Protocols are composable behavior modules. Add them to a content type and they automatically inject database columns, validation rules, and API logic — no code required.

Quick Example

[content_type.implements]
protocols = ["timestampable", "ownable", "soft_deletable", "sortable"]

[[content_type.implements.protocols]]
name = "statusable"
values = "draft,published,archived"
default = "draft"

All 11 Protocols

timestampable

Auto-managed created_at and updated_at timestamps.

ColumnTypeBehavior
created_atTEXTSet on create
updated_atTEXTSet on create, updated on every update
protocols = ["timestampable"]

ownable

Auto-track who created and last modified a record.

ColumnTypeBehavior
created_byINTEGERSet from auth user on create
updated_byINTEGERSet from auth user on every update
protocols = ["ownable"]

soft_deletable

Logical delete — records are marked as deleted instead of being removed. List queries automatically filter deleted_at IS NULL.

ColumnTypeBehavior
deleted_atTEXTSet to current timestamp on delete
deleted_byINTEGERSet from auth user on delete
protocols = ["soft_deletable"]

versionable

Revision tracking with snapshot and restore API.

ColumnTypeBehavior
versionINTEGERIncremented on every update (starts at 1)

Auto-registers revision endpoints:

GET    /admin/cms/{plural}/{id}/revisions                    List revisions
GET    /admin/cms/{plural}/{id}/revisions/{revision_id}      Get revision
POST   /admin/cms/{plural}/{id}/revisions/{revision_id}/restore   Restore revision
GET    /admin/cms/{plural}/{id}/revisions/{rev_a}/diff/{rev_b}   Diff two revisions

Before each update, the current record is snapshotted to content_revisions. On record delete, all revisions are cleaned up.

protocols = ["versionable"]

lockable

Optimistic locking — prevents concurrent write conflicts.

ColumnTypeBehavior
lock_versionINTEGERStarts at 1, incremented on update

On update, the system checks WHERE lock_version = {expected}. If no row matches (another client modified it), returns 409 Conflict.

protocols = ["lockable"]

sortable

Default ordering with a sort key.

ColumnTypeBehavior
sort_keyINTEGERDefault 0, used for ordering

Default sort: sort_key ASC.

protocols = ["sortable"]

Configurable — change the sort field and direction:

[[content_type.implements.protocols]]
name = "sortable"
field = "priority"
direction = "desc"

statusable

State machine with allowed status values.

ColumnTypeBehavior
statusVARCHARValidated against allowed values

String mode (default):

[[content_type.implements.protocols]]
name = "statusable"
values = "draft,published,archived"
default = "draft"

Numeric mode — maps labels to numbers in the database:

[[content_type.implements.protocols]]
name = "statusable"
values = "pending=1,paid=10,shipped=20,completed=30"
default = "1"
mode = "numeric"

In numeric mode, the API accepts labels ("paid") but stores numbers (10).

expirable

Auto-filter expired records.

ColumnTypeBehavior
expires_atTEXTList queries filter expires_at IS NULL OR expires_at > now
protocols = ["expirable"]

nestable

Parent-child tree structure.

ColumnTypeBehavior
parent_idINTEGERReference to parent record
depthINTEGERTree depth (0 = root)
positionINTEGERSibling ordering
protocols = ["nestable"]

tenantable

Multi-tenant isolation.

ColumnTypeBehavior
tenant_idVARCHARAuto-filtered by current tenant (default: "default")

All queries automatically add WHERE tenant_id = ? based on the auth context.

protocols = ["tenantable"]

metaable

Arbitrary JSON metadata.

ColumnTypeBehavior
__metaJSONDefault {}, filterable via __meta.{path} query params
protocols = ["metaable"]

Filter in queries:

curl "http://localhost:9898/api/v1/cms/courses?__meta.featured=true"

Protocol Summary

ProtocolColumns AddedKey Behavior
timestampablecreated_at, updated_atAuto timestamps
ownablecreated_by, updated_byAuto user tracking
soft_deletabledeleted_at, deleted_byLogical delete
versionableversionSnapshot + restore + diff API
lockablelock_versionOptimistic lock (409 on conflict)
sortablesort_keyDefault ordering
statusablestatusState machine
expirableexpires_atAuto-filter expired
nestableparent_id, depth, positionTree structure
tenantabletenant_idMulti-tenant isolation
metaable__metaJSON metadata

Combining Protocols

Protocols are designed to compose freely. A typical blog post:

[content_type.implements]
protocols = ["timestampable", "ownable", "soft_deletable", "versionable", "sortable"]

[[content_type.implements.protocols]]
name = "statusable"
values = "draft,published,archived"
default = "draft"

This gives you: auto timestamps, author tracking, safe delete, revision history, default ordering, and a state machine — all from 6 lines of TOML.

Execution Order

Protocols execute in priority order (lower number = runs first):

PriorityProtocols
-600tenantable
-500ownable
-400timestampable
-300soft_deletable
-200expirable, nestable
-150statusable
-100lockable, sortable
500versionable
1000metaable

This ensures tenant isolation runs first and versioning snapshots run last.

On this page