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.

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.