FUNCTIONS / @SPFN/CMS — TYPE-SAFE CMS FOR NEXT.JS

Content,
typed.

A CMS where the schema is a TypeScript definition. @spfn/cms declares labels in code — nested, multi-language, template-ready — syncs them to the database on server startup, and serves reads from a published cache. Section names and properties are checked at compile time; editors work in drafts and publish when ready. Content is the work that quietly turns into an admin panel. Declared in code, it stays inside the application your agent already reads.

Read the reference Get started $ pnpm spfn add @spfn/cms
01
The premise

The schema lives
in TypeScript.

Labels are declared in code with defineLabels — nested, per-locale, with {placeholder} template variables. That definition is the source of every default and of the types: section names and property paths autocomplete in the IDE, and a typo is a compile error, not a blank string in production. On server startup syncLabels writes the definition to the database — new labels created, changed ones updated by deep-equality check, unchanged ones skipped. The database holds only what editors change on top: a draft, then a published version, resolved as Draft > Published > Default.

defineLabels()
syncLabels @ startup
draft edits
publish
FIG 01 — CODE IS THE DEFAULT · RESOLUTION: DRAFT > PUBLISHED > DEFAULT
02
Core technique

Reads never join.
They're precomputed.

Label content lives in two tables — cms_labels for metadata, cms_label_values for versioned per-locale values — but reads touch neither. Publishing a section rebuilds cms_published_cache: one JSONB row of content per section and locale, stamped with a version. getLabel hits that cache in a single query — 5 ms where the JOIN path takes 87 ms — merges it over the code-defined defaults bound to the current locale, and returns a typed nested object. Only the sections you request are processed, so a 1,000-label app reading one 100-label section does 10% of the work, and the version column drives client-side cache invalidation.

publishSection
cache rebuilt — section × locale JSONB
getLabel: one query
typed nested object
FIG 02 — 5MS VS 87MS WITH JOINS · 17X FASTER · VERSION-BASED INVALIDATION
03
What's inside

The parts of a CMS,
already typed.

Publish it.