<!--
Full-page Markdown export (rendered HTML → GFM).
Source: https://neotoma.io/ar/schemas/versioning
Generated: 2026-04-27T12:50:42.986Z
-->
# Versioning & evolution
Schemas evolve all the time. Versioning is what keeps that evolution safe: every observation carries the schema\_version it was written under, every snapshot stamps the active schema\_version it was computed against, and every breaking change requires a major version bump. New schemas are exported to docs/subsystems/schema\_snapshots/ on every register/activate so the public reference always matches the database.
Wraps the schema registry. Every schema\_registry mutation goes through versioned register / activate / updateSchemaIncremental calls; every mutation triggers an asynchronous snapshot export.
## Schema[#](#schema)
Semantic versioning rules
SQL / TS
Schema or pattern reference for this concept.
1.0.0 // Initial version 1.1.0 // Minor: additive, backward-compatible // - new optional fields // - new converters on existing fields // - non-breaking reducer strategy changes 1.1.1 // Patch: docs, formatting, no schema structure change 2.0.0 // Major: breaking // - removing fields // - changing field types // - making optional fields required // - removing converters // Migration shape schemaRegistry.updateSchemaIncremental({ entity\_type: "contact", fields\_to\_add: \[ { field\_name: "linkedin\_url", field\_type: "string", required: false } \], fields\_to\_remove: \["legacy\_handle"\], // schema\_version auto-increments based on change type activate: true, migrate\_existing: false // historical raw\_fragments stay put unless you backfill });
## Additive by default[#](#additive-by-default)
The common case is additive evolution: new optional fields are added with a minor version bump (1.0.0 → 1.1.0). Existing observations keep their original schema\_version and remain valid; new observations pick up the new fields. Snapshots are recomputed against the active schema, which must always handle missing fields from older observations gracefully.
◆
## Breaking changes are versioned, not destructive[#](#breaking-changes)
Removing a field, changing a type, or making an optional field required is a major version bump (1.x → 2.0). Old observations are immutable, they keep schema\_version 1.x, but new snapshots are computed under 2.0. Removed fields stop appearing in snapshots via reducer schema-projection filtering, but the underlying observation data is preserved. Re-adding a removed field restores it in snapshots without re-ingesting anything.
◆
## updateSchemaIncremental is the safe path[#](#incremental-updates)
updateSchemaIncremental({ fields\_to\_add, fields\_to\_remove, schema\_version?, scope?, activate?, migrate\_existing? }) is the workhorse for runtime evolution. It auto-increments the version (minor for adds, major for removes), can target a user-specific scope, activates the new version atomically, and optionally backfills raw\_fragments into observations for historical data. At least one field must remain after removal, a schema cannot become empty.
◆
## Public schema snapshots[#](#schema-snapshots)
Every register / activate / deactivate triggers an asynchronous export to docs/subsystems/schema\_snapshots/{entity\_type}/v{version}.json. The export merges the latest definitions from src/services/schema\_definitions.ts (authoritative for current code) with historic versions from the schema\_registry table. Failures don't block schema operations. You can also run npm run schema:export manually.
◆
## Deterministic replay across versions[#](#deterministic-replay)
Because observation.schema\_version is immutable, Neotoma can recompute any historical snapshot under any active schema version. This is what enables breaking-change reconciliation, audit, and rollback. Same observations + same active schema + same reducer config ⇒ same snapshot, regardless of when the version was activated.
## Invariants[#](#invariants)
MUST
- Use semantic versioning: minor for additive, major for breaking
- Preserve schema\_version immutably on every observation
- Trigger schema snapshot export on every register / activate / deactivate
- Keep at least one field after removal, schemas cannot become empty
- Ensure the active schema can read observations from all prior versions gracefully
MUST NOT
- Mutate schema\_definition in place, always register a new version
- Skip a major version bump for breaking changes
- Hard-delete observation data when a field is removed, schema-projection filtering handles snapshots
- Allow more than one active version per entity\_type within the same scope
- Block schema operations on snapshot-export failures
## Related[#](#related)
- [Schema registry doc](https://github.com/markmhendrickson/neotoma/blob/main/docs/subsystems/schema_registry.md) , Versioning, migration, updateSchemaIncremental
- [Schema snapshots README](https://github.com/markmhendrickson/neotoma/blob/main/docs/subsystems/schema_snapshots/README.md) , Exported JSON files for every (entity\_type, version) and the changelog
- [Breaking-change reconciliation example](https://github.com/markmhendrickson/neotoma/blob/main/docs/examples/schema_breaking_change_reconciliation.md) , Worked example: how a removed field reconciles in snapshots
- [Schema management workflows](/schema-management) , CLI walkthrough: list, validate, evolve, register
- [Storage layers](/schemas/storage-layers) , Why removed fields stay queryable as raw\_fragments
- [Schema registry](/schemas/registry) , The table that holds every version
## Where to go next[#](#more)
- [All schema concepts](/schemas) , registry, merge policies, storage layers, versioning
- [Primitive record types](/primitives) , sources, observations, snapshots, and the rest of Neotoma's atoms
- [Schema management workflows](/schema-management) , CLI commands for listing, validating, and evolving schemas