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_migrationstable 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-runUnderstanding 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.
| Version | Name | What it adds |
|---|---|---|
| 1001 | control_oauth_signup | OAuth authorization codes, tokens, signup leads |
| 1002 | control_subscriptions | Subscription lifecycle fields |
| 1003 | control_system_config | System configuration key-value store |
| 1004 | add_tenant_slug | Tenant slug for URL routing |
| 1005 | add_password_reset_tokens | Password reset flow |
| 1006 | add_tenant_byoc_configs | BYOC AI provider configuration |
| 1007 | subscriptions_razorpay_billing_leads | Critical — 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.
| Version | Name | What it adds |
|---|---|---|
| 2001 | tenant_org_teams_itsm_governance | Organizations, teams, governance workflows |
| 2002 | add_oes_events_tenant_type_timestamp | Events table indexing |
| 2003 | add_incidents_tenant_status | Incident tenant scoping |
| 2004 | add_automation_jobs_columns | Automation job tracking fields |
| 2005 | add_automation_jobs_tenant_status | Job tenant scoping |
| 2006 | add_vault_rotation | Credential vault rotation tracking |
| 2007 | add_workflow_source_discriminator | Workflow type discrimination |
| 2008 | add_ai_feedback | User feedback on AI responses |
| 2009 | add_autonomous_pipeline | Autonomous remediation pipeline |
| 2010 | add_webhook_deliveries | Webhook delivery tracking |
| 2011 | add_host_inventory | Host hardware/software inventory |
| 2012 | add_gin_index_log_entries | GIN index for full-text log search |
| 2013 | add_runbook_applicability | Runbook applicability rules |
| 2014 | add_report_schedules | Scheduled report configuration |
| 2015 | add_ultimate_platform_tables | Compliance, FinOps, value proof tables |
| 2016 | tenant_integration_sync_config | Integration sync scheduling |
| 2017 | universal_automation_framework | Step-based automation engine |
| 2018 | create_baseline_snapshots | Infrastructure 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:controlChecking 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 existsThis 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:tenantSQLite 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.