CLI
okm reads okmodel.config.ts. okmodel is the same program.
okm reads okmodel.config.ts from the directory it starts in. The okmodel bin is the same program. The file default-exports defineConfig from okmodel/migrate. Each option is a page under Config.
okm push is for a database you can recreate. A shared database uses reviewed migrations: okm generate init, then okm migrate apply.
import { defineConfig } from "okmodel/migrate";
export default defineConfig({
schema: "./schema.ts",
database: process.env.DATABASE_URL!,
});Run
Node.js 22.18 or newer loads the config. bunx okm uses Node. bun --bun okm uses Bun.
Node.js
A process that cannot load a .ts config exits 1. The line is okm needs Node.js >=22.18 to load a .ts config (type stripping). This process is <version>. Run bun --bun okm to use the Bun runtime.
| Invocation | Result |
|---|---|
No arguments, help, --help, or -h | Prints okm <command> and one line per command. |
okm <words> --help | Prints the first command whose name is those words, or starts with them. okm migrate --help prints migrate plan <name>. An unknown name prints the list. |
--version | Prints the package version and does not run a command. The Node.js check still runs first. |
| A failure | Exit code 1. Stderr is error <code>: <message> and a fix line. |
--verbose, or OKM_DEBUG=1 | That failure also prints the stack. |
An unknown command is Unknown command <name>. okm migrate with a subcommand other than plan, apply, status, or check is Unknown command migrate.
Commands
| Command | Result |
|---|---|
build | Writes the catalog under out. |
catalog export | Writes the catalog snapshot a dependent verifies. |
check | Lints the schema. Compares one database when a target is selected. |
dev | Names a target called dev, or creates a PGlite database and prints its directory. |
doctor [code] | Lists triggers, checks roles when they are set, or prints one error code. |
driver test <driver> | Runs the conformance suites against one driver. |
ext list | Lists extensions the server can install. |
ext check | Compares declared extensions with the server. |
ext scaffold <name> | Prints a starting extension() definition, including composite types. --out writes it and never overwrites. |
ext test <file> | Checks that definition against the server. |
generate [name] | Writes the next migration. |
import drizzle <path> | Converts a Drizzle schema into table files. Runs that file. Needs no database. |
migrate plan <name> | Prints the plan. Adds lock estimates when one target answers. --json prints a document. |
migrate apply | Applies pending migrations. |
migrate status | Prints each target's version and state. |
migrate check | Replays migration history in a scratch schema and rolls it back. |
push | Applies the schema diff. Writes no SQL file. |
pull | Reads one database and writes one table file per table, plus the schema file. |
seed <file> | Runs the file's default export on one target. |
Flags
check, generate, push, pull, migrate, seed, doctor, and ext parse the flags in this table. build and dev leave the rest of the line unread. driver test reads --url only, in driver test. pull reads --target, --schema, and --out itself.
| Flag | Read by |
|---|---|
--target <name>, --target=<name> | check, migrate plan, migrate apply, migrate status, migrate check, push, pull, seed, ext, and doctor when roles is set. Who requires it is Database. |
--allow-protected | migrate apply, push, and seed. One invocation. Blocked work is Protection. migrate check ignores the flag. |
--allow-pooler | migrate apply, push, seed, migrate check, and check when it compares a database. Same opt-in as allowPooler. Either one is enough. |
--lock-timeout <milliseconds> | migrate apply. Overrides timeouts.lock. 0 disables it. |
--statement-timeout <milliseconds> | migrate apply. Overrides timeouts.statement. |
--json | migrate plan. Prints a JSON document instead of the SQL text. The lock strings match the human plan, including row estimates. |
--replace <table>.<column>.<old>=<new> | generate and migrate plan. Repeat it for each removed value. =null is the null replacement, on a nullable column. |
--provision | migrate check. Compares a snapshot install with a full replay. |
--schema <name>, --schema=<name> | pull. Repeat it, or separate names with a comma. The default is the target's current schema. |
--out <directory>, --out=<directory> | pull and import drizzle. Writes schema.ts and tables/ there. Without it, the command uses the schema path and the tables directory from the config, or schema.ts and tables/ when import drizzle has no config yet. |
A missing --target name is OKM1853: --target needs a name. A missing --replace value is OKM1541: --replace needs table.column.old=new. --lock-timeout and --statement-timeout need a finite number of milliseconds that is not negative. The message names the flag. check, push, and migrate check refuse a bad number and do not apply it. The same parser runs on the other commands in the list above.
build
okm build validates the schema and writes the out directory. The default is .okm. It prints that directory. It does not connect.
The files are catalog.json, catalog.hash, and types.d.ts. catalog.hash is the SHA-256 of catalog.json. types.d.ts is the row types. The mode header is on Types.
bunx okm buildpull
okm pull reads one database and writes one table file per table, plus the schema file. It does not write the selected namespaces, so a protected target is allowed. It creates a scratch schema to verify functions and views, then drops it. An existing file is OKM1855 and nothing is written. --out <directory> is the other place to write. The default namespace is the target's current schema. --schema <name> selects others and may be repeated.
Export bindings and file stems are unique when folded to lowercase, so Tasks and tasks become Tasks / tasks_2 and do not collide on a case-insensitive volume or in row types. A Windows-reserved stem (con, aux, nul, com1–com9, lpt1–lpt9, any case) gets a _ prefix. The SQL name stays in sqlName when the binding differs. A column named __proto__ is written as a computed key. Single-column foreign keys name the exported table binding, not the raw SQL table name. When oxfmt is missing, the files stay unformatted, a notice is printed, and the report still returns.
Tables, columns, keys, uniques, checks, single-column foreign keys, indexes, identity sequences, enums, domains, extensions, functions, triggers, views, and materialized views are written. A view is SQL text with its columns. Views and BEGIN ATOMIC bodies take dependencies from pg_depend. A plpgsql or SQL-string body gets dependsOn candidates from a lexical match. The report marks each function verified (the scratch replay created it with check_function_bodies on), candidate-only (plpgsql: creation does not prove the body), or unverified (creation failed). A comment above the declaration says the same thing. Policies, roles, grants, aggregates, procedures, event triggers, and rules are printed under not pulled yet with a count, and so is anything table() cannot say. A column type with no builder is that list, with the table and the column. Nothing is dropped without a line.
bunx okm pullbunx okm pull --schema public --out ./adoptedimport drizzle
okm import drizzle <path> reads a Drizzle schema and writes one table file per table, plus the schema file. It imports that path, so the file runs. The drizzle-orm package is the one installed in the project. okmodel does not depend on it. A missing package is OKM1856: install drizzle-orm in the project. An existing file is OKM1855 and nothing is written. The command does not connect.
The report starts with namespace <name>. When the name is not public, set namespace to it. schema() still compiles public. File stems follow the same lowercase-folded uniqueness and Windows-reserved rules as pull. Composite primary-key columns are not null even when Drizzle omitted .notNull() on the column.
Composite foreign keys, relations(), and custom column types are printed under not imported with the table and the reason. A serial column is written as integer not null. The sequence serial would create is listed, not written.
bunx okm import drizzle ./src/db/schema.ts --out ./adoptedcatalog export
okm catalog export writes the column snapshot of the schema this project owns: table names plus each column's name, type, and nullability. It is not the full catalog; that is describe(schema). It does not connect. --json prints the JSON. --out <file> writes that file and prints the path. With neither, the JSON is printed. --json and --out together print the JSON and write the file.
The file has catalogHash and logical (namespace, name, format). A table is a namespace, a name, and columns. There is no function and no connection URL. A dependent lists the path in external.
okm catalog export --out identity.okm.jsoncheck
okm check lints the schema against the previous snapshot, then prints warning findings and ok. An error finding is OKM1510 and the command exits 1. A finding line is <severity> <code> <place>: <reason> -- fix: <fix>. An external table is checked against each external snapshot and, when a target is selected, against that database. A stale or missing snapshot is OKM1841. A foreign key whose namespaces differ is a coupling warning. A foreign key into another database is OKM1842.
A tables directory is scanned for a .ts file whose name is absent from the schema module. That is OKM1024: Table file not in schema: <file>. A schema that would validate on write, with no runtime import of okmodel/validate, is OKM1201. The rules are on Validation.
With one configured target, or with --target, a database that already has okm_meta is compared with the schema. A difference is OKM1520: The database differs from the schema. Several targets and no --target skip that comparison. No target skips it too. An unknown --target is still OKM1845. A protection mismatch on the same database is OKM1852. Both cases are on Database.
dev
okm dev looks for a target named dev. It prints target dev when that target is configured. With no such target it creates a PGlite database at <out>/dev-db, runs select 1, closes it, and prints that directory. The default out is .okm. The command then returns. It does not watch the schema, does not reload, and does not apply migrations.
bunx okm devdriver test
okm driver test <driver> runs the conformance suites against one driver and prints a report. <driver> is pglite, postgresjs, pg, bun, neon, or a path to a module. The module exports name, capabilities, open, and connect. memory: true means one in-process connection. notices: false means the driver does not surface RAISE NOTICE. size is the pool size the stats case expects. Omitted, memory is false, notices is true, and size is 1 for an in-process driver and 4 otherwise. Bun must be >=1.4.0 (package.json engines.bun); a lower Bun stops with a clear message. Each wire and error case drops its okm_* tables before the pool closes.
--url <postgres-url> is the database. --url=<postgres-url> is the same. DATABASE_URL is used when the flag is omitted. pglite does not connect to it. The other built-in drivers need one of the two. The command does not read okmodel.config.ts.
The report starts with driver <name> and dialect postgres. Then one row per case, tab-separated: case id, behavior, result, capability. The result is pass, fail, or unsupported. A case that does not run is unsupported (or fail), never pass. The capability is set on an unsupported row and names the limit (cancel, describe, stream, listen, notices, one connection, no socket to cut). A failure exits 1. An unsupported case does not.
PGlite has no socket to cut, so COMMIT loss is unsupported with capability no socket to cut. Neon uses the same row: the harness cut is a TCP relay, and Neon speaks WebSocket or HTTP.
okm driver test pglite
okm driver test postgresjs --url "$DATABASE_URL"doctor
okm doctor <code> prints one registry entry and does not load the project.
OKM1520: Schema drift
The database has drifted past what an expand migration can repair.
Inspect the diff and repair the database, or generate a migration that matches the drift.An unknown code is Unknown code <code>.
With no code, the command lists each table's triggers, one line <table>: <name>, <name>, in table order. No trigger is no triggers. When roles is set, it also connects and checks external roles, CREATEROLE when a role is managed, and the application role's grants. A missing managed role is left for the plan. That connection is the read-only check class. A protected target is allowed. The command does not change the database.
bunx okm doctor OKM1520ext
okm ext list and okm ext check connect to the selected target as the read-only check class. A protected target is allowed. A pooler host is not refused.
list prints a tab-separated table. The header is name, default, installed, schema.
check compares extensions declared on the schema with what is installed. No declaration is no extensions. A missing install is OKM1811: Extension <name> is not installed on the server. A version the declaration rejects is OKM1812: Extension <name> needs <declared> or higher. The server has <installed>. Install a version the declaration accepts, or change the pin. An exact pin omits "or higher". A match prints name, declared, installed. An omitted pin is -. Declarations are on Extensions.
scaffold <name> reads pg_depend, pg_type, pg_proc, pg_operator, and pg_settings for an extension that is already installed. A missing install is OKM1811. The text is a starting extension() definition the project owns. --out <file> writes that text. An existing file is invalid and nothing is written. Without --out the text is printed.
test <file> loads that module and checks each exported extension(). Every declared type, operator, function, and index method must exist with that signature. A mismatch is invalid and the message lists each one. A requires version or Postgres floor the server does not meet is OKM1812. A match prints ok <name>.
Another name is Unknown command ext <name>.
bunx okm ext checkgenerate
okm generate plans the schema against the last *.catalog.json in name order and writes SQL. The name defaults to migration. Letters, digits, _, and - are accepted. Anything else is Migration name <name> must be letters, digits, _ or -.
The file is <number>_<name>.sql in the migrations directory, with <number>_<name>.catalog.json beside it. The number is one higher than the highest numeric prefix, at least four digits. An empty directory starts at 0001. The name init also writes .gitattributes in the project when that file is missing.
An empty plan writes no SQL file and prints no changes. The out catalog is still written. A written file prints its path, then one line per lint finding. An error finding does not fail this command. migrate plan and check exit 1 on the same finding.
--replace records a removed picklist or enum value. The schema stores no replacement. A chain, a value replaced by another removed value, is OKM1541. =null on a column that is not nullable is OKM1541.
bunx okm generate initbunx okm generate rename-status --replace notes.status.draft=openmigrate plan
okm migrate plan <name> prints the plan and writes nothing. A missing name is okm migrate plan needs a name.
With one target, or with --target, a database that answers adds pg_class row estimates to the lock lines. No target, several targets and no --target, or a target that cannot be reached, prints the plan without estimates. An unknown --target and a protection mismatch still throw. Those are the same refusals as Database. A pooler host is not refused.
An error finding is OKM1510 and the command exits 1 after the plan text. --json prints one document and no SQL text: name, class, each step's class, lock, and sql, and the findings (severity, code, place, reason, fix). lock is the human -- lock: line. The command still exits 1 after that document when a finding is an error. --replace is the same flag as generate.
bunx okm migrate plan initmigrate apply
okm migrate apply applies pending files in the migrations directory. A file without its .catalog.json is refused. With no migration files, or no head snapshot, it prints target <name> and nothing to apply and does not connect.
A pending file is linted after okm_history is read and before any DDL. lintFrom is not read. An error finding is OKM1510. Transaction control is the finding OKM1551. Apply accepts that step when it carries -- okm-allow OKM1551: reason. An empty reason, or a code the statement did not trigger, is OKM1510. migrate check still refuses the step.
The summary is target <name>, then set role <role> when apply issued SET ROLE, then restamped <id> for a history repair, then applied <id> for each file. Nothing applied is nothing to apply. A backfill batch is printed as it commits:
backfill <migration> step <index> batch <n> rows <n> key <last>key is the committed boundary, or - when that batch has none.
A protected target allows expand, reference rows, and provision of an empty database. Contract, unclassified SQL, a backfill, and a history repair that changes a row are OKM1850 until --allow-protected. An empty database, a database that already has objects and no history, and a provision that stopped part-way are on Database. Reference rows are on Reference rows. Timeouts are on Timeouts. Batch size, pause, and the batch statement timeout are on Backfill. Roles are on Roles.
A schema module that does not export a schema() still lets apply run. It installs no reference rows. The message is on Schema.
bunx okm migrate apply --target prodmigrate status
okm migrate status prints every configured target, in name order. --target keeps one name. Several targets do not need --target. No target is OKM1845.
The columns are tab-separated: target, version, catalog, state, protected. protected is true or false. The state is current, behind by expand, behind by contract, ahead by expand, ahead by contract, or failed at step <n> (resume with okm migrate apply).
An unfinished backfill follows as its own section: a line backfill, then the tab-separated header migration, step, rows, key, state.
A pooler host is not refused. The command is read-only, including on a protected target.
bunx okm migrate statusmigrate check
okm migrate check replays every migration into a scratch schema inside one transaction, then rolls that transaction back and drops the schema. The schema name starts with okm_check_. A passing run prints ok <n> migrations. A statement left unrun follows on its own line, <id> step <n>: <reason>. The reasons are VACUUM is not run inside the check transaction, CREATE or DROP DATABASE is not run inside the check transaction, and ALTER SYSTEM is not run inside the check transaction.
A protected target is OKM1850. The message is okm migrate check writes a scratch schema and refuses protected target <name>. Point it at a throwaway Postgres. --allow-protected does not lift it.
The command also checks the previous catalog, the head, and the linter. The first linted id is lintFrom. An error finding is OKM1510. Transaction control (begin, commit, rollback, abort, end, start transaction, prepare transaction), including inside a DO body, is that finding, OKM1551. A function or procedure body is not this. When the finding is allowed, the replay still throws OKM1551: Migration <id> step <n> contains <word>.
--provision also installs the head snapshot and compares it with a full replay. A difference is OKM1521: Provisioning from the snapshot does not match the replayed history.
timeouts apply when the config sets them. The flags do not. A pooler host is refused until allowPooler or --allow-pooler.
bunx okm migrate check --provisionpush
okm push plans the schema against the live catalog on the target and applies that plan as one migration named push-<n>, where n comes from okm_history. It writes no SQL file. An empty plan that already matches okm_meta prints target <name> and no changes. When the hash moved with no DDL, it re-stamps okm_meta and prints restamped push-<n> then nothing to apply. A plan with steps uses the apply summary.
A protected target is OKM1850 until --allow-protected. Lock and statement timeouts stay 5000 and 30000 milliseconds. The config and the flags are not applied. A pooler host is refused until allowPooler or --allow-pooler.
bunx okm pushseed
okm seed <file> loads that module and calls its default export with the testing harness for the selected target. The path is resolved from the project directory. A missing file is Seed file <file> was not found. A default export that is not a function is <file> must default-export a function. A missing file argument is okm seed needs a file.
The target is already migrated. The harness does not apply the catalog. A protected target is OKM1850 until --allow-protected. Several targets need --target. A pooler host is refused until allowPooler or --allow-pooler.
The command prints target <name> and one line <table> <count> for each table the factories inserted, in table name order. No insert is created nothing.
Under rlsTenancy the command runs as the application role. The owner, a superuser, or a BYPASSRLS role is OKM1707.
import type { Testing } from "okmodel/testing";
export default async function seed(t: Testing): Promise<void> {
const f = t.factories({
notes: (x) => ({ title: x.words(2) }),
});
await f.notes.create();
}bunx okm seed seed.ts