Skip to content

Tutorial: your first schema

A hands-on walkthrough from an empty folder to a live, version-controlled table. You’ll scaffold a chkit project, deploy the starter events table it generates, put data in it, query it back, then add a column and ship the change — the full chkit loop, start to finish.

Every step is a real command. Run them in order and you’ll end with a working project you can keep building on.

  • Node.js 20+ or Bun 1.3.5+
  • An email inbox you can reach

No ClickHouse to install: this tutorial claims a free ObsessionDB dev instance straight from the CLI. The commands use bun; npm, pnpm, and yarn work the same way.

Start in a new, empty folder and run chkit init:

Terminal window
mkdir chkit-tutorial
cd chkit-tutorial
bunx chkit@latest init

In an empty directory, init does the full bootstrap: it writes clickhouse.config.ts and a starter schema at src/db/schema/example.ts, creates a package.json, and installs chkit, @chkit/core, and @chkit/plugin-obsessiondb so the project is runnable.

It then shows the connect prompt:

Claim a free ObsessionDB dev instance email code, ready in seconds
I already have an ObsessionDB account log in and pick a service
I already have a ClickHouse instance connect with env vars
Configure later

Choose Claim a free ObsessionDB dev instance, enter your email, and paste the 6-digit code from your inbox. chkit creates a personal organization, provisions a free instance, selects it (written to .chkit/obsessiondb.json), and registers the ObsessionDB plugin in your config.

Confirm the connection works:

Terminal window
bunx chkit query "SELECT 1"

A single row back means you’re connected.

init scaffolded a table for you at src/db/schema/example.ts — an events table that’s a good shape for ingesting application or analytics events:

src/db/schema/example.ts
import { schema, table } from '@chkit/core'
const events = table({
database: 'default',
name: 'events',
engine: 'MergeTree',
columns: [
{ name: 'id', type: 'UInt64' },
{ name: 'source', type: 'String' },
{ name: 'ingested_at', type: 'DateTime64(3)', default: 'fn:now64(3)' },
],
primaryKey: ['id'],
orderBy: ['id'],
partitionBy: 'toYYYYMM(ingested_at)',
})
export default schema(events)

A table() definition maps directly to a ClickHouse CREATE TABLE: a MergeTree engine, three columns, ordered by id, and partitioned by month. ingested_at carries default: 'fn:now64(3)', so the database fills it in automatically. The types here (UInt64, String, DateTime64(3)) are ClickHouse-native; see the Schema DSL reference for the full type system and table options.

Use it as-is for now — you’ll change it later.

chkit generate diffs your schema against the previous snapshot and writes migration SQL. There’s no snapshot yet, so this produces a CREATE TABLE:

Terminal window
bunx chkit generate --name create_events

Open the file it wrote under chkit/migrations/ — chkit shows you the exact SQL before anything is applied:

-- operation: create_table key=table:default.events risk=safe
CREATE TABLE default.events
(
id UInt64,
source String,
ingested_at DateTime64(3) DEFAULT now64(3)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(ingested_at)
ORDER BY id;
Terminal window
bunx chkit migrate --apply

This runs the pending migration against the instance you claimed and records it in the migration journal.

Check migration state, confirm the live schema matches your code, and look at the table directly:

Terminal window
bunx chkit status
bunx chkit check
bunx chkit query "DESCRIBE events"

status lists the applied migration, check confirms the database matches your TypeScript definitions, and DESCRIBE shows the live columns.

The table is empty. Put a couple of rows in with chkit queryingested_at is left out, so the database fills it from its default:

Terminal window
bunx chkit query "INSERT INTO events (id, source) VALUES (1, 'web'), (2, 'mobile')"

Then read them back:

Terminal window
bunx chkit query "SELECT count() FROM events"
bunx chkit query "SELECT id, source, ingested_at FROM events ORDER BY id"

You now have a schema in code and matching data in a live database.

Schemas change. Add a level column to the events table in src/db/schema/example.ts:

columns: [
{ name: 'id', type: 'UInt64' },
{ name: 'source', type: 'String' },
{ name: 'level', type: 'String' },
{ name: 'ingested_at', type: 'DateTime64(3)', default: 'fn:now64(3)' },
],

Generate a migration for the change and review it — this time it’s an ALTER TABLE, not a recreate:

Terminal window
bunx chkit generate --name add_level_column
-- operation: add_column key=table:default.events risk=safe
ALTER TABLE default.events ADD COLUMN level String AFTER source;

Apply it and confirm the column landed:

Terminal window
bunx chkit migrate --apply
bunx chkit check
bunx chkit query "DESCRIBE events"

check passes again, and DESCRIBE now lists level. That’s the whole chkit loop: edit the schema → generate → review the SQL → migrate → verify — repeat it for every change from here on.