Skip to content

Schema Migrations

For a practical guide on evolving schemas across deployments, see Evolving Schemas in Production.

As your application evolves, your graph schema changes:

  • Adding features: New node types, new properties, new relationships
  • Refactoring: Renaming types, changing property formats
  • Deploying safely: Ensuring schema changes don’t break running applications

Without schema management, you’d face:

  • No way to know if the database matches your code
  • Silent failures when property names change
  • Manual migration scripts for every deployment

TypeGraph’s schema management:

  1. Stores the schema in the database alongside your data
  2. Detects changes between your code and the stored schema
  3. Auto-migrates safe changes (adding types, optional properties)
  4. Blocks breaking changes until you handle them explicitly

TypeGraph stores your graph schema in the database, enabling version tracking, safe migrations, and runtime introspection.

When you create a store with createStoreWithSchema(), TypeGraph:

  1. Serializes your graph definition to JSON
  2. Compares it with the stored schema (if any)
  3. Returns the result so you can act on it

When you create a store, TypeGraph can automatically manage schema versions:

import { createStoreWithSchema } from "@nicia-ai/typegraph";
const [store, result] = await createStoreWithSchema(graph, backend);
switch (result.status) {
case "initialized":
console.log(`Schema initialized at version ${result.version}`);
break;
case "unchanged":
console.log(`Schema unchanged at version ${result.version}`);
break;
case "migrated":
console.log(`Migrated from v${result.fromVersion} to v${result.toVersion}`);
break;
case "pending":
console.log(`Safe changes pending at version ${result.version}`);
break;
case "breaking":
console.log("Breaking changes detected:", result.actions);
break;
}

TypeGraph provides two ways to create a store:

Use createStore() when you manage schema versions yourself:

import { createStore } from "@nicia-ai/typegraph";
const store = createStore(graph, backend);
// No schema versioning - you handle migrations manually

Managed Store (Automatic Schema Management)

Section titled “Managed Store (Automatic Schema Management)”

Use createStoreWithSchema() for automatic version tracking:

import { createStoreWithSchema } from "@nicia-ai/typegraph";
const [store, result] = await createStoreWithSchema(graph, backend, {
autoMigrate: true, // Auto-apply safe changes (default: true)
throwOnBreaking: true, // Throw on breaking changes (default: true)
onBeforeMigrate: (context) => {
console.log(`Migrating ${context.graphId} from v${context.fromVersion} to v${context.toVersion}`);
},
onAfterMigrate: (context) => {
console.log(`Migration complete: v${context.toVersion}`);
},
});

The validation result indicates what happened during store initialization:

StatusMeaning
initializedFirst run - schema version 1 was created
unchangedSchema matches stored version - no changes
migratedSafe changes auto-applied, new version created
pendingSafe changes detected but autoMigrate is false
breakingBreaking changes detected, action required

These changes are backwards compatible and can be auto-migrated:

  • Adding new node types
  • Adding new edge types
  • Adding optional properties with defaults
  • Adding new ontology relations

These changes require manual migration:

  • Removing node or edge types
  • Renaming node or edge types
  • Changing property types
  • Removing properties
  • Changing cardinality constraints to be more restrictive

When breaking changes are detected:

const [store, result] = await createStoreWithSchema(graph, backend, {
throwOnBreaking: false, // Don't throw, inspect instead
});
if (result.status === "breaking") {
console.log("Breaking changes detected:");
console.log("Summary:", result.diff.summary);
console.log("Required actions:");
for (const action of result.actions) {
console.log(` - ${action}`);
}
// Option 1: Fix your schema to be backwards compatible
// Option 2: Force migration (data loss possible!)
// import { migrateSchema } from "@nicia-ai/typegraph/schema";
// await migrateSchema(backend, graph, currentVersion);
}

Query the stored schema at runtime:

import { getActiveSchema, isSchemaInitialized, getSchemaChanges } from "@nicia-ai/typegraph/schema";
// Check if schema exists
const initialized = await isSchemaInitialized(backend, "my_graph");
// Get the current schema
const schema = await getActiveSchema(backend, "my_graph");
if (schema) {
console.log("Graph ID:", schema.graphId);
console.log("Version:", schema.version);
console.log("Nodes:", Object.keys(schema.nodes));
console.log("Edges:", Object.keys(schema.edges));
}
// Preview changes without applying
const diff = await getSchemaChanges(backend, graph);
if (diff?.hasChanges) {
console.log("Pending changes:", diff.summary);
console.log("Is backwards compatible:", !diff.hasBreakingChanges);
}

For full control over migrations:

import {
initializeSchema,
migrateSchema,
rollbackSchema,
ensureSchema,
} from "@nicia-ai/typegraph/schema";
// Initialize schema (first run only)
const row = await initializeSchema(backend, graph);
console.log("Created version:", row.version);
// Migrate to new version
const newVersion = await migrateSchema(backend, graph, currentVersion);
console.log("Migrated to version:", newVersion);
// Rollback to a previous version
await rollbackSchema(backend, "my_graph", 1);
console.log("Rolled back to version 1");
// Or use ensureSchema for automatic handling
const result = await ensureSchema(backend, graph, {
autoMigrate: true,
throwOnBreaking: true,
});

Schemas are stored as JSON documents with computed hashes for fast comparison:

import { serializeSchema, computeSchemaHash } from "@nicia-ai/typegraph/schema";
// Serialize a graph definition
const serialized = serializeSchema(graph, 1);
// Compute hash for comparison
const hash = computeSchemaHash(serialized);

The serialized schema includes:

  • Graph ID and version
  • All node types with their Zod schemas (as JSON Schema)
  • All edge types with endpoints and constraints
  • Complete ontology relations
  • Uniqueness constraints and delete behaviors

TypeGraph maintains a history of all schema versions:

typegraph_schema_versions
├── version 1 (initial)
├── version 2 (added User node)
├── version 3 (added email property) ← active
└── ...

Only one version is marked as “active” at a time. Previous versions are preserved for auditing and potential rollback.

// Production: Use schema management
const [store, result] = await createStoreWithSchema(graph, backend);
// Development: Basic store is fine for rapid iteration
const store = createStore(graph, backend);
async function initializeApp() {
const [store, result] = await createStoreWithSchema(graph, backend);
if (result.status === "breaking") {
console.error("Database schema incompatible with application!");
console.error("Run migrations before deploying this version.");
process.exit(1);
}
if (result.status === "migrated") {
console.log(`Schema auto-migrated to v${result.toVersion}`);
}
return store;
}
import { getSchemaChanges } from "@nicia-ai/typegraph/schema";
// In your CI/CD pipeline or migration script
const diff = await getSchemaChanges(backend, graph);
if (diff?.hasChanges) {
console.log("Schema changes detected:");
console.log(diff.summary);
if (!diff.isBackwardsCompatible) {
console.error("Breaking changes require manual migration!");
process.exit(1);
}
}

When adding new properties, always provide defaults to ensure backwards compatibility:

// Good: Optional with default
const User = defineNode("User", {
schema: z.object({
name: z.string(),
// New property with default - safe migration
status: z.enum(["active", "inactive"]).default("active"),
}),
});
// Bad: Required without default - breaking change
const User = defineNode("User", {
schema: z.object({
name: z.string(),
status: z.enum(["active", "inactive"]), // No default!
}),
});