DEV Community

Seiya Izumi
Seiya Izumi

Posted on

Kyrage: A TypeScript-First Database Migration Tool for Modern Development

When working on personal projects with CockroachDB, popular tools like Drizzle and Prisma presented unexpected challenges. Drizzle's CockroachDB support was still in planning stages, and while PostgreSQL compatibility seemed promising, migration attempts resulted in data type-related errors. Prisma had its own set of CockroachDB-specific issues.

Most traditional Node.js migration tools require manually writing SQL migration files. What was missing was a tool that could automatically generate migrations based on schema differences while keeping everything in the TypeScript ecosystem - something that could provide the declarative approach similar to Ruby's ridgepole or Atlas, but with better integration for Node.js projects.

Creating Kyrage

This gap led to the creation of Kyrage (kirāju) - a personal project designed to fill this specific need.

GitHub logo IzumiSy / kyrage

A minimal, schema-based declarative migration tool for Node.js ecosystem

kyrage

Test NPM Version License: MIT Node.js Version

A minimal, schema-based declarative migration tool for Node.js ecosystem

kyrage (kirāju) automatically generates and applies database migrations by comparing your TypeScript schema definitions with your actual database state. No more writing migration files by hand!

Why kyrage?

Traditional database migrations require manually writing up/down migration files every time you change your schema. This is error-prone and time-consuming.

kyrage takes a different approach:

  1. ✍️ Define your desired schema in TypeScript
  2. 🔍 kyrage compares it with your actual database
  3. 🚀 Automatically generates the necessary migrations
  4. ✅ Apply migrations with a single command

This is a style of managing database schema that is called as Versioned Migration Authoring by Atlas.

📦 Installation

# Install globally
npm install -g @izumisy/kyrage

# Or use with npx
npx @izumisy/kyrage --help
Enter fullscreen mode Exit fullscreen mode

🚀 Quick Start

1. Create Configuration File

Create a kyrage.config.ts file in your project root:

import { defineConfig } from "@izumisy/kyrage";
export default defineConfig
Enter fullscreen mode Exit fullscreen mode

Since most development work happens in Next.js + TypeScript environments, the goal was to unify everything within a TypeScript codebase while providing a minimal, schema-based declarative migration tool.

How Kyrage Works

Kyrage automatically generates and applies database migrations by comparing your TypeScript schema definitions with your actual database state.

The tool follows what Atlas calls the "Versioned Migration Authoring" approach:

  1. Connect to your database and introspect the current schema
  2. Compare your TypeScript schema definition with the current state
  3. Generate a migration file (in JSON format) based on the differences
  4. Apply the migration automatically

Under the hood, kyrage leverages Kysely's migration functionality for SQL execution and version management, while providing its own CockroachDB dialect support.

Key Features

Schema Definition

Define your database schema using intuitive TypeScript syntax. The schema definition benefits from Kysely's type system, providing excellent IDE autocompletion and type safety.

import { column as c, defineTable as t } from "@izumisy/kyrage";

export const members = t("members", {
  id: c("uuid", { primaryKey: true }),
  email: c("text", { unique: true }),
  name: c("text", { unique: true }),
  age: c("integer", { nullable: true }),
  createdAt: c("timestamptz"),
});

export const posts = t("posts", {
  id: c("uuid", { primaryKey: true }),
  author_id: c("uuid"),
  title: c("text"),
  content: c("text"),
  published: c("boolean", { default: false }),
  published_at: c("timestamptz", { nullable: true }),
});
Enter fullscreen mode Exit fullscreen mode

Kyrage also supports indexes, foreign key constraints, composite primary keys, and composite unique constraints:

export const posts = t(
  "posts",
  {
    id: c("uuid"),
    author_id: c("uuid"),
    slug: c("text", { notNull: true }),
    title: c("text"),
    content: c("text", { notNull: true }),
  },
  (t) => [
    // Composite primary key
    t.primaryKey(["id", "author_id"]),

    // Unique constraint with custom name
    t.unique(["author_id", "slug"], {
      name: "unique_author_slug",
    }),

    // Foreign key with cascade delete
    t.reference("author_id", members, "id", {
      onDelete: "cascade",
      name: "posts_author_fk"
    }),

    // Unique index
    t.index(["slug", "title"], { unique: true })
  ]
);
Enter fullscreen mode Exit fullscreen mode

Dev Database Support

Kyrage includes built-in mechanism to spin up ephemeral database for development as Docker container. This addresses common challenges in team development environments.

To use this feature, add the dev configuration to your kyrage config:

export default defineConfig({
  database: {
    dialect: "postgres",
    connectionString: "postgres://postgres:password@localhost:5432/mydb",
  },
  dev: {
    container: {
      image: "postgres:17"
    }
  },
  tables: [members, posts],
});
Enter fullscreen mode Exit fullscreen mode

Then generate migrations using the dev database:

# Generate migration using a clean, ephemeral database
$ kyrage generate --dev
🚀 Starting dev database for migration generation...
✔ Dev database started: postgres
-- create_table: users
✔ Migration file generated: migrations/1755525514175.json
✔ Dev database stopped
Enter fullscreen mode Exit fullscreen mode

This feature eliminates environment dependencies by removing the need to share production database credentials with developers. Each developer works with a clean slate, preventing conflicts that arise when multiple team members make schema changes simultaneously. The approach also reduces the risk of accidental production data modifications while ensuring consistency across the team since everyone uses the same Docker image.

The implementation uses Testcontainers internally, making it seamless to run in CI/CD environments like GitHub Actions.

Getting Started

Installation and Setup

# Install kyrage
npm install @izumisy/kyrage
Enter fullscreen mode Exit fullscreen mode

Create configuration file

kyrage.config.ts:

import { defineConfig } from "@izumisy/kyrage";
import { members, posts } from "./schema";

export default defineConfig({
  database: {
    dialect: "postgres",
    connectionString: "postgres://postgres:password@localhost:5432/mydb",
  },
  tables: [members, posts],
});
Enter fullscreen mode Exit fullscreen mode

Basic Workflow

# Generate migration
$ npx @izumisy/kyrage generate
-- create_table: members (id, email, name, age, createdAt) 
-- create_table: posts (id, author_id, title, content, published, published_at)
✔ Migration file generated: migrations/1754372124127.json

# Preview SQL before applying
$ npx @izumisy/kyrage apply --plan
create table "members" ("id" uuid not null primary key, "email" text not null unique, ...)
create table "posts" ("id" uuid not null primary key, "author_id" uuid not null, ...)

# Apply migration
$ npx @izumisy/kyrage apply
✔ Migration applied: 1754372124127
Enter fullscreen mode Exit fullscreen mode

Advanced Features

Persistent Dev Databases

Kyrage provides the ability to persist dev database containers across kyrage sessions, enabling applications to continuously connect to the same dev database container managed by kyrage

# Start persistent dev database
$ kyrage dev start
✔ Applied 2 migrations
✨ Dev database ready: postgresql://postgres:password@localhost:32768/test

# Get connection URL for your app
$ DATABASE_URL=$(kyrage dev get-url) npm run dev

# Check running containers
$ kyrage dev status
Running: abc123def456 (postgres:17)

# Clean up when done
$ kyrage dev clean
Enter fullscreen mode Exit fullscreen mode

Migration Squashing

For iterative development, kyrage supports squashing multiple migrations to clean up the migration history before applying to production. This feature treats the Dev Database like a feature branch - you can generate multiple migrations during development iterations, then squash them into a clean, consolidated migration when ready to merge to the main database.

Here's how the workflow looks:

# During feature development - generate multiple migrations
$ kyrage generate --dev
✔ Migration file generated: migrations/001_add_users_table.json

$ kyrage generate --dev  
✔ Migration file generated: migrations/002_add_email_column.json

$ kyrage generate --dev
✔ Migration file generated: migrations/003_add_email_index.json

# Before merging to main - squash into a single clean migration
$ kyrage generate --squash
✔ Squashed 3 migrations into: migrations/004_user_management_feature.json

# Apply the clean, squashed migration to production
$ kyrage apply
✔ Migration applied: 004_user_management_feature
Enter fullscreen mode Exit fullscreen mode

This helps organize changes before applying them to production.

Environment-Specific Configuration

Thanks to the unjs/c12 configuration system, kyrage supports environment-specific settings:

export default defineConfig({
  $development: {
    database: {
      dialect: "postgres",
      connectionString: "postgres://dev:dev@localhost:5432/myapp_dev"
    },
    dev: {
      container: { image: "postgres:17" }
    }
  },

  $production: {
    database: {
      dialect: "postgres",
      connectionString: process.env.DATABASE_URL!
    }
  },

  tables: []
});
Enter fullscreen mode Exit fullscreen mode

Why Choose Kyrage?

Kyrage offers several compelling advantages over traditional migration tools. Its TypeScript-first design keeps everything in your familiar development ecosystem with excellent IDE support and type safety. The declarative approach means you define your desired database state and kyrage figures out how to get there, eliminating the tedious process of writing manual migration files.

The Dev Database support removes common team development friction by providing isolated environments for each developer. Rather than reinventing database migration fundamentals, kyrage builds upon proven tools like Kysely while adding modern conveniences that align with contemporary development practices.

And let's be honest - if you're building applications with CockroachDB in the TypeScript ecosystem, kyrage is pretty much your only option that actually works! While other popular tools struggle with CockroachDB compatibility, kyrage was specifically designed with this use case in mind, thanks to Kysely's excellent dialect extensibility!

TypeScript All the Way

Kyrage is designed as a minimal tool focused solely on schema migrations - it doesn't include query building capabilities. This intentional design pairs perfectly with Kysely and kysely-codegen to create the ultimate TypeScript database development experience.

The recommended workflow combines kyrage for schema management with kysely-codegen for type-safe query building:

# Spin up Dev Database to get url
$ kyrage dev start

# Update your schema and generate migration (automatically apply the changes)
$ kyrage generate --dev

# Generate type-safe query builder types from your updated schema
$ DATABASE_URL="$(kyrage dev get-url)" kysely-codegen
Enter fullscreen mode Exit fullscreen mode

This combination gives you declarative schema management through kyrage while leveraging Kysely's powerful type-safe query building capabilities. Other similar libraries that generate type-safe query builders from database schemas work equally well in this setup, allowing you to choose the tools that best fit your project's needs.

Future Roadmap

The kyrage project has several exciting features planned that will significantly enhance the developer experience:

  • Export API: Reverse-engineer existing databases into TypeScript schemas - perfect for migrating legacy projects to kyrage
  • Multi-database support: Expanding to DuckDB, SQLite, and MySQL for broader ecosystem compatibility
  • Hooks API: Execute custom logic during migrations (automated backups, notifications, CI/CD integration)
  • Seed data pre-population: Automatically populate development databases with test data for streamlined development

These features aim to make kyrage not just a migration tool, but a complete database development workflow solution.

Getting Involved

Kyrage is open source and ready for you to try! Whether you're tired of writing migration files by hand, struggling with CockroachDB compatibility, or just want a cleaner TypeScript-first database workflow, kyrage might be exactly what you've been looking for.

The project welcomes contributors, feedback, and real-world usage stories as it continues to evolve.

Top comments (0)