Versioning & migrations
Versioning & migrations
Every resource.json carries a schemaVersion. When crouton evolves the shape of resource.json, older files are auto-migrated to the current version in the dev environment; anything that can't be brought current is reported on the status page rather than crashing boot. A generated JSON Schema gives you editor autocomplete and inline validation, and a draft flag lets a resource live in the repo without being served.
schemaVersion
{
"$schema": "https://ghentcdh.github.io/crouton/schema/v1/resource.schema.json",
"schemaVersion": 1,
"name": "book",
"route": "books"
}schemaVersion is a plain, monotonically increasing integer. It is not tied to the crouton package version β it only changes when the shape of resource.json changes in a way that needs a migration. A file written before versioning existed (no field) is treated as the baseline (version 1).
The version crouton understands is CURRENT_RESOURCE_VERSION, exported from @ghentcdh/crouton-core. Files generated by the CLI are stamped with it automatically (see $schema and stamping).
Auto-migration on load (dev only)
When crouton loads resources, each resource.json is checked against CURRENT_RESOURCE_VERSION before it is parsed. Migration only runs in the dev environment β it rewrites the checked-in file, so it needs a writable, version-controlled checkout where the change can be reviewed and committed. Dev mode is enabled with the CROUTON_SCHEMA_EDITOR env var (the same flag that powers the visual resource builder).
| File version vs current | Dev (CROUTON_SCHEMA_EDITOR=true) | Non-dev |
|---|---|---|
| equal | loads normally | loads normally |
| older | migrated β validated β rewritten on disk β loaded; any failure is reported | reported as failed (no migration runs) |
| newer than crouton | reported as failed (upgrade crouton) | reported as failed |
Because the dev loader re-reads resources on every request, a freshly migrated file is picked up immediately β no restart needed. The rewrite goes through the same raw-preserving serializer used elsewhere, so untouched keys stay byte-stable and the git diff is minimal and reviewable.
Outside dev, an out-of-date file is never rewritten or run from unpersisted config. The intended flow is: migrate and commit in dev, so production only ever sees files already at the current version. A file that reaches production still out of date shows up on the status page as failed β a signal that the migration was never committed.
What "failed" looks like
A file that can't be brought current (a missing migration step, a step that throws, or a post-migration validation error) is skipped β that resource is not served β and listed on the status page with its version, the expected version, and the error. See Resource load health.
Authoring a migration
Migrations live in ../../../packages/crouton-core/src/lib/resource/migrations. Each step is a small, pure function that transforms the raw JSON object (before any Zod normalisation) and returns a new object:
// migrations/0001-to-0002.ts
import type { ResourceMigration } from './types';
export const migration_0001to0002: ResourceMigration = {
from: 1,
to: 2,
description: 'rename display.customComponent β display.component',
migrate: (raw) => {
const display = raw['display'] as Record<string, unknown> | undefined;
if (!display || !('customComponent' in display)) return raw; // guard: safe on already-migrated files
const { customComponent, ...rest } = display;
return { ...raw, display: { ...rest, component: customComponent } };
},
};To ship it:
- Add the step to the
MIGRATIONSarray inmigrations/index.ts. - Bump
CURRENT_RESOURCE_VERSIONinmigrations/../version.tsto the newtovalue. - Rebuild
crouton-coreso the JSON Schema regenerates.
Rules that keep migrations safe:
- Operate on raw JSON, never the parsed config. Migrating the transformed object would normalise
columnsmapβarray and inject defaults, exploding diffs. - Return a new object; never mutate the input.
- Guard on the old shape (
'customComponent' in display) so a partially- or already-migrated file is a no-op. - One step per version. The chain must be contiguous from baseline to current β a gap is caught by the unit tests.
$schema and stamping {#schema-and-stamping}
A JSON Schema is generated from the same definition crouton validates against, so your editor can autocomplete keys and flag invalid values as you type β the JSON equivalent of an XSD.
Files generated by crouton update resources are stamped with a $schema URL and the current schemaVersion automatically. For hand-written files, add the $schema key yourself, or configure it workspace-wide so no per-file key is needed:
// .vscode/settings.json
{
"json.schemas": [
{
"fileMatch": [
"**/resource.json"
],
"url": "./node_modules/@ghentcdh/crouton-core/dist/resource.schema.json"
}
]
}The schema ships inside the @ghentcdh/crouton-core package (dist/resource.schema.json) so autocomplete works offline and always matches the installed version. Two files are emitted on every build:
resource.schema.jsonβ the "latest" pointer, always the current version.resource.schema.v<N>.jsonβ a frozen snapshot of each version, kept for track-back. Older snapshots are never rewritten, so any historical version stays recoverable.
The same schema is also published to this docs site, so you can point $schema at a stable public URL without installing the package:
https://ghentcdh.github.io/crouton/schema/v<N>/resource.schema.jsonβ the versioned, canonical URL that generated files reference.https://ghentcdh.github.io/crouton/schema/resource.schema.jsonβ the latest pointer.
Prefer a versioned URL (or the packaged copy) over main-branch raw links, so a file always validates against the schema for its own version.
Autocomplete, not a second validator The JSON Schema covers structure and types for editor ergonomics. It does
not enforce cross-field rules or the runtime normalisation (e.g. table defaulting to model). The Zod schema in crouton-core remains the source of truth; the JSON Schema is a generated mirror β never hand-edit it.
Draft resources
Set draft: true to keep a resource in the repo without serving it:
{
"schemaVersion": 1,
"name": "book",
"route": "books",
"draft": true
}A draft is excluded from the loaded set entirely β it doesn't appear in the sidebar, has no CRUD or schema endpoints, and its route 404s like any unknown resource. It is still auto-migrated in dev (so it stays current while you work on it), and it shows on the status page as an informational "draft β not loaded" row so it's visible but clearly excluded rather than silently missing.
New resources scaffolded by crouton update resources default to draft: true, so a generated-but-unreviewed resource never goes live by accident. Pass --no-draft to opt out, or flip the flag to false (or remove it) once the resource is ready.