@strifeapp/schema
v0.14.0
Published
Template schema merge logic using json-merge-patch (RFC 7396).
Keywords
Readme
@strifeapp/schema
Typed schema-as-code for Strife. Declare templates with defineType + fields.*, register them with defineConfig + glob, infer content shapes with Infer<typeof X>.
Status: alpha. The DSL surface is stabilising in v1.x. Pin the
0.0.1-alpha.xline if you depend on it from CI.
Install
npm install @strifeapp/schemaRequires Node >=22 and ESM-compatible tooling.
Declaring a template
import { defineType, fields } from '@strifeapp/schema';
export const article = defineType({
name: 'articles',
title: 'Article',
type: 'document',
fields: {
heading: fields.text({ label: 'Heading', localizable: true }),
body: fields.html({ label: 'Body' }),
publishedAt: fields.date({ label: 'Published' }),
archived: fields.switch({ label: 'Archived' }),
},
});The result is a runtime no-op — defineType returns its input unchanged. The work happens at the type level: Infer<typeof article> walks the field shape.
Inferring content types
import type { Infer } from '@strifeapp/schema';
type Article = Infer<typeof article>;
// ^? {
// heading: Record<string, string>; // localizable → keyed by locale
// body: string; // non-localizable → scalar
// publishedAt: string;
// archived: boolean;
// }Use Article as the type for content stored against the articles template.
Registering schemas with defineConfig + glob
The Strife CLI loads schemas via a strife.config.ts (or .tsx, .mts, .js, .mjs) at the project root:
// strife.config.ts
import { defineConfig, glob } from '@strifeapp/schema';
export default defineConfig({
schema: glob('./schemas/**/*.ts'),
});defineConfig and glob are runtime no-ops — they return their input. The CLI consumes the descriptor at push time, resolves the pattern via tinyglobby, and imports each matched file.
glob() paths resolve relative to the config file's directory (not the cwd). Pass base to pin a different root:
defineConfig({
schema: glob('*.ts', { base: './content/types' }),
});Localization config
defineConfig also accepts an optional closed-set localization block. On strife push it is materialized into the team's Configurations/Localization singleton — the document the content indexes, API, and synced client read to drive per-locale output.
export default defineConfig({
schema: glob('./schemas/**/*.ts'),
localization: {
defaultLocale: 'en',
locales: ['en', 'sv'], // codes follow [a-z]{2}(-[A-Z]{2})? — e.g. en, sv, pt-BR
fallbackToPrimary: true, // fall back to defaultLocale when a value is missing
},
});Push semantics — silent full replace: the config is the source of truth. Pushing a locales list that omits a previously-published locale removes it from the singleton, which re-indexes content and stops emitting that locale. Likewise, omitting fallbackToPrimary resets it to false (it is not preserved from a prior push). The codes here must match the codes your content is keyed by; a config locale with no matching content emits an empty per-locale entry. The server validates the code format and that defaultLocale is one of locales (a bad push is rejected, never written).
Config-key → document mapping:
| defineConfig key | RavenDB singleton |
| ------------------ | -------------------------- |
| localization | Configurations/Localization |
Future scale-space
defineConfig's type intentionally leaves room for more closed-set config keys (e.g. media) and v2 fields like plugins, tools, and environments. Additions are non-breaking via TypeScript structural typing.
What compile() produces
compile(definition) is the wire-format translator the CLI uses internally. It maps the DSL to Strife's RavenDB Template JSON. You won't usually call it directly — the CLI does that during strife push — but it's exported for tooling and tests.
Pushing a schema
See @strifeapp/cli for the full strife push flow.
