Schema Migrations
For a practical guide on evolving schemas across deployments, see Evolving Schemas in Production.
When Do You Need Schema Management?
Section titled “When Do You Need Schema Management?”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:
- Stores the schema in the database alongside your data
- Detects changes between your code and the stored schema
- Auto-migrates safe changes (adding types, optional properties)
- Blocks breaking changes until you handle them explicitly
How It Works
Section titled “How It Works”TypeGraph stores your graph schema in the database, enabling version tracking, safe migrations, and runtime introspection.
When you create a store with createStoreWithSchema(), TypeGraph:
- Creates the base tables if the database is fresh (auto-bootstrap)
- Serializes your graph definition to JSON
- Compares it with the stored schema (if any)
- Returns the result so you can act on it
Schema Lifecycle
Section titled “Schema Lifecycle”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;}Basic vs Managed vs Verified Store
Section titled “Basic vs Managed vs Verified Store”TypeGraph provides three ways to create a store, each suited to a different deployment role:
Basic Store (No Schema Management)
Section titled “Basic Store (No Schema Management)”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 manuallyManaged 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}`); },});Verified Store (Zero-DDL Attach With Verification Gate)
Section titled “Verified Store (Zero-DDL Attach With Verification Gate)”Use createVerifiedStore() at runtime when the application runs under a
least-privilege, DML-only database role and a separate privileged step
has already advanced the schema. It is the runtime counterpart of
createStoreWithSchema(): a synchronous-semantics attach that issues
no DDL and fails fast if the database is not at the same schema
version as the code graph.
import { createVerifiedStore } from "@nicia-ai/typegraph";
// Runtime — least-privilege, DML-only role. Zero DDL.const [store, result] = await createVerifiedStore(graph, backend);// result.status === "unchanged" on success.It throws:
ConfigurationErrorif no schema has been initialized (run the privileged migration step first).MigrationErrorif the persisted schema is behind the code graph by any pending change (safe or breaking) — the least-privilege runtime cannot migrate.StoreNotInitializedErrorif the schema is current but the runtime-contribution markers (e.g. fulltext) are missing/stale.
If you only need the check without building a Store (e.g. a readiness
probe), call assertSchemaCurrent(backend, graph) directly — it returns
the same SchemaValidationResult or throws the same errors.
Schema Validation Results
Section titled “Schema Validation Results”The validation result indicates what happened during store initialization:
| Status | Meaning |
|---|---|
initialized | First run - schema version 1 was created |
unchanged | Schema matches stored version - no changes |
migrated | Safe changes auto-applied, new version created |
pending | Safe changes detected but autoMigrate is false |
breaking | Breaking changes detected, action required |
The initialized and migrated results also include
committedRow: SchemaVersionRow, the schema row that was just written. Most
applications only need the version fields shown above, but integrations that
build schema metadata can use committedRow without issuing another
getActiveSchema read.
Safe vs Breaking Changes
Section titled “Safe vs Breaking Changes”Safe Changes (Auto-Migrated)
Section titled “Safe Changes (Auto-Migrated)”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
Breaking Changes (Require Manual Action)
Section titled “Breaking Changes (Require Manual Action)”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
Handling Breaking Changes
Section titled “Handling Breaking Changes”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);}Schema Introspection
Section titled “Schema Introspection”Query the stored schema at runtime:
import { getActiveSchema, isSchemaInitialized, getSchemaChanges } from "@nicia-ai/typegraph/schema";
// Check if schema existsconst initialized = await isSchemaInitialized(backend, "my_graph");
// Get the current schemaconst 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 applyingconst diff = await getSchemaChanges(backend, graph);if (diff?.hasChanges) { console.log("Pending changes:", diff.summary); console.log("Is backwards compatible:", !diff.hasBreakingChanges);}Manual Migration
Section titled “Manual Migration”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 versionconst newVersion = await migrateSchema(backend, graph, currentVersion);console.log("Migrated to version:", newVersion);
// Rollback to a previous versionawait rollbackSchema(backend, "my_graph", 1);console.log("Rolled back to version 1");
// Or use ensureSchema for automatic handlingconst result = await ensureSchema(backend, graph, { autoMigrate: true, throwOnBreaking: true,});Schema Serialization
Section titled “Schema Serialization”Schemas are stored as JSON documents with computed hashes for fast comparison:
import { serializeSchema, computeSchemaHash } from "@nicia-ai/typegraph/schema";
// Serialize a graph definitionconst serialized = serializeSchema(graph, 1);
// Compute hash for comparisonconst 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
Version History
Section titled “Version History”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.
Best Practices
Section titled “Best Practices”1. Use Managed Stores in Production
Section titled “1. Use Managed Stores in Production”// Production: Use schema managementconst [store, result] = await createStoreWithSchema(graph, backend);
// Development: Basic store is fine for rapid iterationconst store = createStore(graph, backend);2. Check Migration Status on Startup
Section titled “2. Check Migration Status on Startup”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;}3. Preview Changes Before Deployment
Section titled “3. Preview Changes Before Deployment”import { getSchemaChanges } from "@nicia-ai/typegraph/schema";
// In your CI/CD pipeline or migration scriptconst 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); }}4. Add Properties with Defaults
Section titled “4. Add Properties with Defaults”When adding new properties, always provide defaults to ensure backwards compatibility:
// Good: Optional with defaultconst 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 changeconst User = defineNode("User", { schema: z.object({ name: z.string(), status: z.enum(["active", "inactive"]), // No default! }),});