To perform schema migrations in ClickHouse with chkit, you declare your tables in TypeScript or Python, chkit generates the migration SQL from the diff, applies it, and fails your CI if the live database deviates from what’s declared. We tested version 0.2.0-beta.11 end-to-end, including the parts that should fail.
yoDEV Tested · October 22, 2026 We tested chkit 0.2.0-beta.11 (TypeScript with Bun) and chkit-py 0.2.0 against clickhouse-server 26.9.25.15 on Linux. The results we report as ours come from that test.
What is chkit?
chkit is schema as code for ClickHouse, MIT-licensed: you describe tables, views, materialized views, and dictionaries in code, and the CLI calculates the necessary CREATE and ALTER statements. In its Show HN it’s presented as “like Drizzle,” and the workflow has that shape: a schema file, a generate step that writes SQL you can read, a migrate step that applies it, and a log table that records what was executed.
It’s distributed across several npm packages: chkit (the CLI), @chkit/core (the schema DSL, configuration, and diff engine) and plugins to import an existing schema, generate types, and run backfills. There’s also a Python version, chkit-py.
Two things you should know before adopting it.
It’s in beta. The README claims the CLI and schema DSL are stable and that chkit already runs production workloads, but the team reserves small breaking changes to the user experience and internal APIs before 1.0. Everything you see in this article is pinned to 0.2.0-beta.11, published to npm on October 12, 2026.
A vendor builds it. chkit comes from the ObsessionDB team, a managed ClickHouse service, and the README recommends ObsessionDB for production. The tool itself is MIT and works with any ClickHouse. The relationship shows in chkit init, which offers you to sign up for ObsessionDB before explaining how to connect your own server.
How do you install chkit?
With Bun, pinning the version we tested (the README command is the same without the @ versions):
bun add -d chkit@0.2.0-beta.11 @chkit/core@0.2.0-beta.11
bunx chkit init
init creates clickhouse.config.ts and a sample schema in src/db/schema/example.ts. The generated configuration reads the connection from four environment variables and only defines the clickhouse block when CLICKHOUSE_URL is set:
export CLICKHOUSE_URL=http://localhost:8123
export CLICKHOUSE_USER=default
export CLICKHOUSE_PASSWORD=
export CLICKHOUSE_DB=default
All four have to be defined; an empty password is allowed, an undefined one is not. The sample schema also refuses to load without CLICKHOUSE_DB, so a misconfigured terminal fails visibly instead of generating migrations for the wrong database.
For Python it’s pip install chkit-py, and the command is still chkit. Below you’ll see how it differs.
How do you define a ClickHouse database schema in TypeScript?
You define it with the table() function from @chkit/core. This is the table that init generates:
import { schema, table } from '@chkit/core'
const database = process.env.CLICKHOUSE_DB
if (!database?.trim()) {
throw new Error('Set CLICKHOUSE_DB to the intended database before generating migrations.')
}
const events = table({
database,
name: 'events',
engine: 'MergeTree',
columns: [
{ name: 'id', type: 'UInt64' },
{ name: 'source', type: 'String' },
{ name: 'ingested_at', type: 'DateTime64(3)', default: { expression: 'now64(3)' } },
],
primaryKey: ['id'],
orderBy: ['id'],
partitionBy: 'toYYYYMM(ingested_at)',
})
export default schema(events)
Column types are ClickHouse’s own written as text, so there’s no type translation layer to learn. One detail that surprised us: a default written as plain text is a literal value. In our first test we wrote default: "''" for an empty value and got DEFAULT '''''', that is, the two-character text ''. Expressions go in { expression: '...' } (the old form from the README, 'fn:now64(3)', still works and is equivalent). The DSL reference documents it, and generate rejects the most common mistake: a function call written as text, like 'now64(3)', in a column that can’t hold text.
How do you run schema migrations in ClickHouse?
Run generate with a name and then migrate:
bunx chkit generate --name init
bunx chkit migrate --apply
bunx chkit status
generate writes a timestamped SQL file in chkit/migrations/ and updates chkit/meta/snapshot.json. This is the file we got:
-- chkit-migration-format: v1
-- generated-at: 2026-10-23T00:17:16.148Z
-- cli-version: 0.2.0-beta.11
-- definition-count: 1
-- operation-count: 2
-- rename-suggestion-count: 0
-- risk-summary: safe=2, caution=0, danger=0
-- operation: create_database key=database:default risk=safe
CREATE DATABASE IF NOT EXISTS default;
-- operation: create_table key=table:default.events risk=safe
CREATE TABLE IF NOT EXISTS default.events
(
`id` UInt64,
`source` String,
`ingested_at` DateTime64(3) DEFAULT now64(3)
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(ingested_at)
PRIMARY KEY (`id`)
ORDER BY (`id`);
Each statement carries a risk label, and the SQL is normal ClickHouse DDL that you can review in a pull request. migrate --apply executed it and logged it in the _chkit_migrations table, with the file name, a SHA-256 checksum of the file, and the chkit version that applied it.
Adding a column to the schema and running generate --name add-country produced a single statement:
-- operation: alter_table_add_column key=table:default.events:column:country risk=safe
ALTER TABLE default.events ADD COLUMN IF NOT EXISTS `country` LowCardinality(String);
How do you detect schema drift in ClickHouse?
With chkit drift, which inspects the live database and compares it to the snapshot. To test it we did what someone always ends up doing: add a column by hand, outside of chkit.
ALTER TABLE default.events ADD COLUMN debug_flag UInt8
chkit drift detected it:
Expected objects: 1
Actual objects: 1
Drifted: yes
Table shape drift:
- default.events
extraColumns=debug_flag
reasonCodes=extra_column
With --json it returns the same in structured form, with a tableDrift entry per table and a reason code for each type of difference: missing or extra columns, changed types or defaults, settings, indexes, TTL, engine, primary key, ORDER BY, PARTITION BY, and projections.
Note: drift always exits with code 0. It reports, it doesn’t block. The command that fails a build is chkit check.
Tables that chkit doesn’t manage are treated differently. We created a scratch table by hand, and drift listed it as an unexpected object but still indicated Drifted: no, and check passed. It’s intentional: chkit can share a database with tables it doesn’t own. If chkit owns the entire database, the check.failOnExtraObjects policy makes extra objects fail too.
How do you use chkit check in CI with GitHub Actions?
chkit check exits with code 1 if there are pending migrations, if someone edited an already-applied migration file, or if the live schema drifted. We triggered all three cases:
- After adding the
countrycolumn but before migrating:Failed checks: pending_migrations, schema_drift, code 1. - After migrating:
Check status: ok, code 0. - After the
debug_flagcolumn added by hand:Failed checks: schema_drift, code 1. - After adding a line to an already-applied migration file:
Failed checks: checksum_mismatch, code 1.chkit statusnamed the file; reverting the change resolved it.
The GitHub Actions workflow for pull requests that the documentation brings is:
name: Schema Validation
on: pull_request
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.3.5'
- run: bun install --frozen-lockfile
- name: Check schema consistency
run: bunx chkit check --strict --json
- name: Verify generated types
run: bunx chkit codegen --check --json
--strict enables all policies regardless of the configuration file, which is what you want in CI. One requirement the example doesn’t show: check needs to reach a ClickHouse server, because the migration log lives there. Without a connection it stops with “clickhouse config is required for check”. The deployment example from the same guide passes CLICKHOUSE_URL, CLICKHOUSE_USER, CLICKHOUSE_PASSWORD, and CLICKHOUSE_DB from the secrets; the validation job needs them too.
What does chkit do with destructive migrations?
It blocks them, unless you say otherwise. We removed the source column from the schema and generate wrote:
-- operation: alter_table_drop_column key=table:default.events:column:source risk=danger
ALTER TABLE default.events DROP COLUMN IF EXISTS `source`;
When running migrate --apply without a terminal, as in CI, it refused with a warning code (drop_column_irreversible), the reason, the impact, and a recommendation, and asked us to repeat with --allow-destructive. With that option, it applied.
One difference from the documentation: it says a blocked destructive migration exits with code 3 in non-interactive runs. In our tests it exited with 3 only with --json; in text mode, with closed input, and also with CI=true, it exited with 1. Both fail the job, so nothing slips through, but if your pipeline separates code 3 to send destructive changes to a job with manual approval, as the documentation suggests, use --json.
Does chkit generate TypeScript types from ClickHouse tables?
Yes, with @chkit/plugin-codegen. With the plugin registered in clickhouse.config.ts (codegen({ outFile: './src/generated/chkit-types.ts' })), chkit codegen wrote:
export type DefaultEventsRow = {
id: string
country: string
ingested_at: string
}
UInt64 and DateTime64 come out as string, which matches how ClickHouse returns them in JSON. The plugin can also generate Zod schemas, and chkit codegen --check fails CI when the generated file is out of date.
Does chkit work with Python?
Yes, with a difference worth knowing. pip install chkit-py installs version 0.2.0, which the documentation marks as alpha. init, generate, migrate --apply, and check worked in our test.
The difference is in drift. By default, chkit drift in Python compares the snapshot to your local schema files, not the live database. After adding the same column by hand, drift in Python indicated Drifted: no, and chkit check --strict exited with code 0. Adding --live, both detected it: drift --live reported extra_column and check --live failed with schema_drift, code 1.
As of October 22, 2026, --live only appears on the Kafka page of the documentation, and the Python CI workflow from the guide runs chkit check --strict --json without that option, while claiming the options are the same as in TypeScript. If you use the Python version in CI, add --live.
Is chkit like Drizzle ORM?
In workflow, yes; in scope, no. Drizzle ORM is the TypeScript ORM with which many teams define their schema and generate migrations for relational databases. chkit copies that cycle (schema in code, generate, migrate) and brings it to ClickHouse, but it’s not an ORM: it doesn’t build queries from your code. If your team already writes the Postgres schema in Drizzle and ClickHouse DDL is still the only thing done by hand, that’s the gap it fills.
What other database migration tools are there for ClickHouse?
ClickHouse’s own knowledge base lists a dozen. Most are imperative executors that apply versioned SQL files in order: golang-migrate, Goose, Flyway, Liquibase, and ClickHouse-specific executors for Node.js and Python. Atlas and Ptah are declarative like chkit and also detect drift.
What chkit brings is the TypeScript-focused approach: the schema lives in the same language as your application, types are generated from it, and a single CLI covers generate, migrate, drift, and CI validation. And if what you’re looking for is to explore that database from the browser, we already saw how to deploy LibreDB Studio with Docker or Helm.
Is chkit worth using?
If you use ClickHouse alongside TypeScript code and your schema changes today live in loose SQL files or in someone’s memory, chkit deserves an afternoon. The generated SQL reads well, the protection against destructive changes works, and drift detection found a column added by hand that would have gone unnoticed otherwise.
Pin the version, because 1.0 might still bring small breaking changes. And if you work in Python, treat that version as the alpha it says it is and use --live.
What about you? What schema change done by hand in production cost you the most to discover?
- Versioned SQL files with a migration executor
- A declarative tool (Atlas, chkit, or another)
- By hand, straight in the console
- I prefer another option (tell us which)
Comment below or, if this article reached you by email, reply directly to the email: your reply is published here.
Related: Explore your ClickHouse database from the browser: how to deploy LibreDB Studio with Docker or Helm