Skip to content

@kosan/cli

Migration orchestrator that runs database migrations across all active tenants — in parallel, with per-tenant failure isolation.

bash
npm install -D @kosan/cli

kosan migrate

bash
npx kosan migrate [options]

Options

FlagDefaultDescription
--config <path>kosan.config.tsPath to config file
--concurrency <n>5Max parallel tenant migrations
--tenant <slug>all activeTarget a single tenant by slug

Examples

bash
# Migrate all active tenants (5 at a time)
npx kosan migrate --config kosan.config.ts

# Migrate 10 tenants at a time
npx kosan migrate --concurrency 10

# Migrate only the "acme" tenant
npx kosan migrate --tenant acme

Configuration file

Create kosan.config.ts (or .js / .mjs) at the root of your project:

ts
import { Sequelize } from 'sequelize';
import { SequelizeMasterStore } from '@kosan/sequelize';
import { SequelizeAdapter } from '@kosan/sequelize';
import { TenantRegistry } from '@kosan/core';
import type { KosanConfig } from '@kosan/cli';

const master = new Sequelize(process.env.MASTER_DATABASE_URL!, { logging: false });
const masterStore = await SequelizeMasterStore.create(master);
const adapter = new SequelizeAdapter({ defaultDialect: 'postgres' });
const registry = await TenantRegistry.create({ master: masterStore, adapter });

const config: KosanConfig = {
  registry,
  migrationsPath: './migrations',          // directory containing migration files
  migrationsTableName: 'sequelize_meta',   // default
  concurrency: 5,                          // default — overridden by --concurrency flag
};

export default config;

KosanConfig

ts
interface KosanConfig {
  registry: TenantRegistry;
  migrationsPath: string;
  migrationsTableName?: string; // default: 'sequelize_meta'
  concurrency?: number;         // default: 5
}

Migration files

Each file in migrationsPath must export up and down functions:

ts
import type { QueryInterface, DataTypes } from 'sequelize';

export async function up(qi: QueryInterface): Promise<void> {
  await qi.createTable('orders', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true,
    },
    description: { type: DataTypes.STRING, allowNull: false },
    total:       { type: DataTypes.DECIMAL(10, 2), allowNull: false },
    status: {
      type: DataTypes.ENUM('pending', 'paid', 'cancelled'),
      defaultValue: 'pending',
    },
    createdAt: { type: DataTypes.DATE },
    updatedAt: { type: DataTypes.DATE },
  });
}

export async function down(qi: QueryInterface): Promise<void> {
  await qi.dropTable('orders');
}

Files are sorted alphabetically — use a numeric prefix (001_, 002_, …) to control order.

Supported extensions: .ts, .js, .mjs, .cjs.


Result output

After running, the CLI prints a table:

Tenant       Outcome    Applied    Duration
-----------  ---------  ---------  --------
acme         ok         2          312ms
globex       ok         2          289ms
initech      error      0          41ms

Error for initech: connect ECONNREFUSED 192.168.1.100:5432
  • Outcome: ok or error
  • Applied: number of migrations that ran
  • Duration: wall-clock time for that tenant

Failed tenants do not stop other tenants from completing.


Programmatic API

You can run migrations from application code without the CLI:

ts
import { runMigrate, loadMigrationsFromDir, printResults } from '@kosan/cli';
import { Sequelize } from 'sequelize';

const migrations = loadMigrationsFromDir('./migrations', Sequelize);

const results = await runMigrate({
  registry,
  migrations,
  concurrency: 3,
  migrationsTableName: 'sequelize_meta',
});

printResults(results);

Migration interface

ts
interface Migration {
  name: string;
  up: (qi: QueryInterface) => Promise<void>;
  down: (qi: QueryInterface) => Promise<void>;
}

TenantMigrationResult

ts
interface TenantMigrationResult {
  tenant: TenantConfig;
  outcome: 'ok' | 'error';
  applied: number;
  durationMs: number;
  error?: Error;
}

Using with a custom logger

Pass a logger to printResults to redirect output:

ts
import pino from 'pino';

const logger = pino();

printResults(results, {
  log: (line) => logger.info(line),
});

CI integration

In a CI pipeline, kosan migrate exits with code 0 if all tenants succeed and 1 if any tenant fails. Use --concurrency 1 for sequential runs:

yaml
- name: Run tenant migrations
  run: npx kosan migrate --concurrency 1
  env:
    MASTER_DATABASE_URL: ${{ secrets.MASTER_DATABASE_URL }}

Released under the MIT License.