Skip to Content
AdministrationDatabase Migrations

Database Migrations

AtlasAI uses a versioned migration system to evolve the database schema over time. This page explains how migrations work, when they run, and how to run them manually if needed.


How migrations work

Every change to the database schema — adding a table, adding a column, creating an index — is captured as a numbered migration file. When the Tenant Plane or Control Plane starts, it checks which migrations have already been applied and runs any that are new.

Key properties of the migration system:

  • Versioned — each migration has a unique version number. The schema_migrations table records which have been applied.
  • Idempotent — migrations use CREATE TABLE IF NOT EXISTS, ADD COLUMN IF NOT EXISTS, and similar safe SQL. Running a migration twice does nothing harmful.
  • Additive only — migrations never drop columns or tables that existing code depends on. Removing deprecated schema objects happens only as part of major version releases, with advance notice.
  • Forward-only — rolling back to a previous version of TP does not undo schema changes. The old code simply ignores the new columns.

When migrations run

Automatically on startup

By default, the Tenant Plane applies any pending migrations as part of its startup sequence. You do not need to do anything.

Via the API

You can trigger migrations manually via the API without restarting the service:

curl -X POST https://your-tenant-plane/api/db/migrate \ -H "x-service-secret: $INTERNAL_SERVICE_SECRET"

This is useful after a database restore or when deploying in environments where the startup migration is disabled.

Via the command line

From the repository root:

# Apply all pending migrations (control DB + tenant DB) CONTROL_PLANE_DATABASE_URL=postgresql://... \ TENANT_PLANE_DATABASE_URL=postgresql://... \ npm run migrate:versioned # Apply only tenant DB migrations npm run migrate:versioned:tenant # Apply only control DB migrations npm run migrate:versioned:control # Dry run: see what would be applied without changing anything npm run migrate:versioned:dry-run

Understanding the output

When you run migrations manually, the output shows what happened:

=== AtlasAI Versioned Migration Runner === Mode: APPLY [CONTROL] Connecting to database... [CONTROL] Connected. [CONTROL] Applied 2 migration(s): 1006, 1007 [CONTROL] Skipped 5 already-applied migration(s) [CONTROL] All migrations completed successfully. [TENANT] Connecting to database... [TENANT] Connected. [TENANT] Skipped 18 already-applied migration(s) [TENANT] All migrations completed successfully. [OK] Migration run complete.
  • Applied — migrations that were new and have been applied
  • Skipped — migrations that were already applied on a previous run
  • Errors — migrations that failed (rare; see Troubleshooting below)

Migration registry

Migrations are organized into two groups:

Control Plane migrations (versions 1001–1099)

These apply to the database used by the Control Plane. Most self-hosted customers do not need to run these unless they are also self-hosting the Control Plane.

VersionNameWhat it adds
1001control_oauth_signupOAuth authorization codes, tokens, signup leads
1002control_subscriptionsSubscription lifecycle fields
1003control_system_configSystem configuration key-value store
1004add_tenant_slugTenant slug for URL routing
1005add_password_reset_tokensPassword reset flow
1006add_tenant_byoc_configsBYOC AI provider configuration
1007subscriptions_razorpay_billing_leadsCritical — adds 9 columns to billing_invoices and 5 columns to signup_leads

Tenant Plane migrations (versions 2001–2099)

These apply to the database used by the Tenant Plane. Self-hosted deployments must apply these.

VersionNameWhat it adds
2001tenant_org_teams_itsm_governanceOrganizations, teams, governance workflows
2002add_oes_events_tenant_type_timestampEvents table indexing
2003add_incidents_tenant_statusIncident tenant scoping
2004add_automation_jobs_columnsAutomation job tracking fields
2005add_automation_jobs_tenant_statusJob tenant scoping
2006add_vault_rotationCredential vault rotation tracking
2007add_workflow_source_discriminatorWorkflow type discrimination
2008add_ai_feedbackUser feedback on AI responses
2009add_autonomous_pipelineAutonomous remediation pipeline
2010add_webhook_deliveriesWebhook delivery tracking
2011add_host_inventoryHost hardware/software inventory
2012add_gin_index_log_entriesGIN index for full-text log search
2013add_runbook_applicabilityRunbook applicability rules
2014add_report_schedulesScheduled report configuration
2015add_ultimate_platform_tablesCompliance, FinOps, value proof tables
2016tenant_integration_sync_configIntegration sync scheduling
2017universal_automation_frameworkStep-based automation engine
2018create_baseline_snapshotsInfrastructure baseline snapshots

Critical migrations for existing deployments

If you are upgrading from a version prior to 1.7.0, the following migration is especially important:

Version 1007: subscriptions_razorpay_billing_leads

This migration adds columns that are required for:

  • The Invoice feature (billing portal, invoice history)
  • The Leads admin feature (demo request management)

Without this migration, these features will return errors. Run it immediately:

CONTROL_PLANE_DATABASE_URL=postgresql://... npm run migrate:versioned:control

Checking migration status

You can inspect the schema_migrations table directly:

-- Connect to your database psql postgresql://username:password@hostname:5432/atlas -- See all applied migrations SELECT version, name, applied_at FROM schema_migrations ORDER BY version;

Expected output for a fully migrated database:

version | name | applied_at ---------+--------------------------------------------+------------------------------- 1001 | control_oauth_signup | 2026-01-15 10:23:45.123456+00 1002 | control_subscriptions | 2026-01-15 10:23:45.234567+00 1003 | control_system_config | 2026-01-15 10:23:45.345678+00 ...

Troubleshooting

”Column already exists” error

error: column "subscription_id" of relation "billing_invoices" already exists

This error is harmless. It means the column was added outside the migration system (e.g., during a previous manual SQL run). The migration system catches this and marks the migration as skipped.

Migration fails with “permission denied”

The database user needs CREATE TABLE, CREATE INDEX, and ALTER TABLE privileges:

-- Grant required permissions GRANT CREATE ON DATABASE atlas TO atlasusr; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO atlasusr; GRANT ALL PRIVILEGES ON SCHEMA public TO atlasusr;

Migration hangs or takes too long

Large tables (millions of rows) can make index creation slow. Use CREATE INDEX CONCURRENTLY if you need to add an index without locking:

-- Check for blocking queries SELECT pid, now() - query_start AS duration, query FROM pg_stat_activity WHERE state = 'active' AND query_start < now() - interval '30 seconds';

Schema out of sync after restore

After restoring a database backup, the schema may be at an older state than what the code expects. Run migrations to bring it forward:

curl -X POST https://your-tenant-plane/api/db/migrate \ -H "x-service-secret: $INTERNAL_SERVICE_SECRET"

Or via the command line:

npm run migrate:versioned:tenant

SQLite note (development and small deployments)

In development or single-instance deployments without a Postgres URL, the Tenant Plane uses SQLite by default. SQLite migrations run automatically on startup. The same version tracking applies — the schema_migrations table is created in the SQLite file.

SQLite cannot be shared across multiple TP replicas. For multi-replica (HA) deployments, PostgreSQL is required. See High Availability for details.