How Ghost Manages Database Migrations Using knex-migrator and DatabaseStateManager

Ghost delegates database migration execution to the knex-migrator package while the DatabaseStateManager class orchestrates the process by detecting the current database state and deciding whether to initialize a fresh instance or apply pending versioned migrations.

The TryGhost/Ghost repository implements a robust, version-aware database migration workflow that runs automatically during application startup. Understanding how knex-migrator integrates with the DatabaseStateManager is essential for developers maintaining Ghost installations or contributing to the core codebase. This architecture separates the concerns of state detection and migration execution, ensuring safe, idempotent database transitions across different environments.

Boot Integration with DatabaseStateManager

When the Ghost server initializes, ghost/core/core/boot.js instantiates the DatabaseStateManager rather than calling knex-migrator directly. This abstraction provides a clean interface for bringing the database to a usable state before the application accepts requests.

// ghost/core/core/boot.js
const DatabaseStateManager = require('./server/data/db/database-state-manager');
const dbStateManager = new DatabaseStateManager({
    knexMigratorFilePath: config.get('paths:appRoot')
});
await dbStateManager.makeReady();   // ← orchestrates init / migrate

The manager receives the application root path to locate the migration configuration, encapsulating all database preparation logic within a single makeReady() call.

Detecting Database State

The DatabaseStateManager.getState() method queries knex-migrator to determine the current database condition by invoking knexMigrator.isDatabaseOK(). This method translates specific error codes from the migrator into four distinct internal states:

  • READY – The database schema matches the current Ghost version
  • NEEDS_INITIALISATION – No migration table exists or the database has never been bootstrapped
  • NEEDS_MIGRATION – The migration table exists but the schema version lags behind the latest migration folder
  • ERROR – Unexpected database errors wrapped as Ghost errors and reported to Sentry
// ghost/core/core/server/data/db/database-state-manager.js
async getState() {
    try {
        await this.knexMigrator.isDatabaseOK();   // succeeds → READY
        return states.READY;
    } catch (error) {
        if (error.code === 'DB_NOT_INITIALISED' ||
            error.code === 'MIGRATION_TABLE_IS_MISSING') {
            return states.NEEDS_INITIALISATION;
        }
        if (error.code === 'DB_NEEDS_MIGRATION') {
            return states.NEEDS_MIGRATION;
        }
        // …wrap unknown errors…
    }
}

This state detection mechanism allows Ghost to handle fresh installations differently from upgrade scenarios without requiring manual intervention.

Initializing and Migrating the Database

The makeReady() method in ghost/core/core/server/data/db/database-state-manager.js implements the migration orchestration logic. After logging the detected state, it executes the appropriate action based on the database condition.

Database Initialization

When the state is NEEDS_INITIALISATION, the manager calls knexMigrator.init() to create the migration table, execute scripts from the init migration folder, and record the current Ghost version in the metadata.

Applying Pending Migrations

When the state is NEEDS_MIGRATION, the manager invokes knexMigrator.migrate() to walk the versioned migration folders under ghost/core/core/server/data/migrations/. The migrator applies any scripts newer than the recorded version and updates the migration metadata accordingly.

// ghost/core/core/server/data/db/database-state-manager.js
async makeReady() {
    const state = await this.getState();
    printState({state});

    if (state === states.NEEDS_INITIALISATION) {
        await this.knexMigrator.init();
    }
    if (state === states.NEEDS_MIGRATION) {
        await this.knexMigrator.migrate();
    }

    // Verify final state
    const finalState = await this.getState();
    printState({state: finalState});
}

After performing the required operation, the manager re-verifies the state to confirm the database has reached READY status before allowing the boot process to continue.

Migration Script Organization

Migration scripts reside in ghost/core/core/server/data/migrations/, organized into folders named after Ghost versions (e.g., v5.0.0). Each folder contains standard Knex.js migration files that create tables, alter schemas, add indexes, or seed static data. The knex-migrator package executes these scripts sequentially based on semantic version ordering, ensuring migrations run in the correct dependency order.

CLI and Manual Migration Options

Developers can trigger the same migration logic manually using npm scripts defined in the repository:

pnpm knex-migrator init    # creates a fresh DB (used for tests)

pnpm knex-migrator migrate # applies any pending migrations

These commands invoke the same knex-migrator package used by DatabaseStateManager, ensuring consistency between automated startup behavior and manual database maintenance.

Programmatic Migration Example

For testing or custom scripts, you can invoke the migrator directly:

// Example: triggering a migration programmatically
const KnexMigrator = require('knex-migrator');
const migrator = new KnexMigrator({knexMigratorFilePath: process.cwd()});

await migrator.migrate();   // runs all pending versioned migrations

Checking Database State Programmatically

// Example: checking DB state from a script
const DatabaseStateManager = require('./ghost/core/core/server/data/db/database-state-manager');
const dbState = new DatabaseStateManager({knexMigratorFilePath: process.cwd()});

const state = await dbState.getState();
console.log('Current DB state:', state); // 0-READY, 1-NEEDS_INITIALISATION, 2-NEEDS_MIGRATION

Summary

  • DatabaseStateManager acts as the primary interface during Ghost boot, abstracting the complexity of knex-migrator while providing clear state detection through getState().
  • Four distinct states (READY, NEEDS_INITIALISATION, NEEDS_MIGRATION, ERROR) determine whether the system initializes a new database or applies pending migrations via makeReady().
  • Migrations are stored in versioned folders under ghost/core/core/server/data/migrations/ and executed sequentially by knex-migrator based on semantic versioning.
  • The same migration logic is available through CLI commands and programmatic APIs, ensuring consistency across automated deployments and manual maintenance.

Frequently Asked Questions

What is the difference between knex-migrator and DatabaseStateManager?

knex-migrator is the underlying npm package that executes SQL migration scripts and manages the migration table, while DatabaseStateManager is a Ghost-specific wrapper class that determines the appropriate action (initialization vs. migration) and handles error translation. The manager calls knex-migrator methods like isDatabaseOK(), init(), and migrate() based on the detected database state, adding Ghost-specific logging and error handling.

Where does Ghost store its database migration scripts?

Ghost stores migration scripts in ghost/core/core/server/data/migrations/ in folders named after version numbers (e.g., v5.0.0). Each folder contains standard Knex.js migration files that the migrator executes sequentially when the DatabaseStateManager determines that NEEDS_MIGRATION state exists.

How does Ghost handle database errors during startup?

The DatabaseStateManager catches specific error codes from knex-migrator (DB_NOT_INITIALISED, MIGRATION_TABLE_IS_MISSING, DB_NEEDS_MIGRATION) and maps them to internal states, while unexpected errors are wrapped as Ghost errors and reported to Sentry. If the database reaches an unrecoverable error state, the boot process fails before the application starts accepting traffic.

Can I run Ghost database migrations manually without starting the full application?

Yes, Ghost exposes knex-migrator through npm scripts allowing manual execution via pnpm knex-migrator init for fresh databases or pnpm knex-migrator migrate for pending upgrades. These commands use the same configuration and migration folders as the automated DatabaseStateManager, ensuring identical behavior whether run manually or during boot.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →