Config
defineConfig is the file okm reads. Each option is a heading on this page, except database and roles.
schema: "./schema.ts",required database: process.env.DATABASE_URL!,or targets …,optionsdefineConfig is the file okm reads. okm reads okmodel.config.ts from the project directory. The file default-exports defineConfig from okmodel/migrate.
Refused
A missing okmodel.config.ts fails the import. An export that is not an object with a string schema is refused: okmodel.config.ts must default-export defineConfig(...).
Runtime
Node.js 22.18 or newer loads the config. bunx okm uses Node. bun --bun okm uses Bun.
The declaration
import { defineConfig } from "okmodel/migrate";
export default defineConfig({
schema: "./schema.ts",
database: process.env.DATABASE_URL!,
});| Order | Option | You use it for |
|---|---|---|
| 1 | Schema | schema is required. okm loads this file. |
| 2 | Database | database is one target named default. targets replaces it. |
| 3 | Tables | tables is a directory okm check reads for orphan table files. |
| 4 | Migrations | migrations is where okm generate writes SQL. |
| 5 | Out | out is where okm build writes the catalog. |
| 6 | Timeouts | timeouts sets lock_timeout and statement_timeout for apply. |
| 7 | Pooler | allowPooler skips the pooler-host refusal. |
| 8 | Lint from | lintFrom is the first migration id okm migrate check lints. |
| 9 | Backfill | backfill sets batch size, pause, and batch statement timeout. |
| 10 | Roles | roles names migration, app, and optional reporting grants. |
| 11 | Namespace | namespace is the Postgres schema this catalog owns. |
| 12 | External | external lists catalog snapshots this project verifies. |
Schema
schema is required. okm loads this file. okm build reads it and does not connect.
A value that is not a string refuses the config. The code is invalid. The message is okmodel.config.ts must default-export defineConfig(...).
import { defineConfig } from "okmodel/migrate";
export default defineConfig({
schema: "./schema.ts",
database: process.env.DATABASE_URL!,
});path
A path is resolved from the project directory, the directory okm starts in. An absolute path stays absolute. A relative path, including .., is resolved from that directory.
export
okm imports the file and reads its exports. A value with a tables array and catalog is the schema. schema() returns that value. When several exports match, the name that sorts first is used.
A file with no such export is refused. The code is invalid. The message is <path> must export a schema(). <path> is the schema string.
On that refusal, okm migrate apply still runs. It installs no reference rows.
Tables
tables is a directory okm check reads for orphan table files. The name is the file name without .ts. That text has to appear in the schema module source.
| Case | Result |
|---|---|
| omitted | The directory is not read. |
a .ts name absent from the schema module | OKM1024. |
not .ts | Skipped. |
a name that starts with . | Skipped. |
okm check lists the files in that directory. A folder inside is not opened. A missing directory is an empty list.
import { defineConfig } from "okmodel/migrate";
export default defineConfig({
schema: "./schema.ts",
tables: "./tables",
database: process.env.DATABASE_URL!,
});OKM1024
Table file not in schema: extra.ts. Add the file's table to the schema() call that okm check reads.
Migrations
migrations is where okm generate writes SQL. okm migrate apply reads the same directory.
| Value | Result |
|---|---|
| omitted | The directory migrations. |
| a string | That path, resolved from the project directory. |
omitted
Leave migrations out. Generate writes a file such as 0001_init.sql, using \n as the line ending, and writes 0001_init.catalog.json beside it. The number is one higher than the highest numeric prefix in the directory, padded to four digits. With no prefix, the file starts at 0001. An empty plan writes nothing in this directory.
string
Set migrations and generate and apply use that directory.
import { defineConfig } from "okmodel/migrate";
export default defineConfig({
schema: "./schema.ts",
migrations: "migrations",
});Apply loads every *.sql file in name order, together with its .catalog.json. A SQL file without that snapshot is refused. For 0001_init.sql the message is Migration 0001_init is missing its catalog snapshot.
okm migrate check reads this directory and runs the statements it is given. Run it as a non-superuser. The first migration id it lints is lintFrom.
A step that cannot run inside a transaction is committed on its own. If that step fails, okm migrate status still prints failed at step N (resume with okm migrate apply). The next apply probes the database before it runs the statement again. A hand-written step can declare that probe with one -- probe: line, a read-only SELECT that returns true when the change is already in place. When the probe cannot tell whether the step landed, apply stops and okm migrate resolve is how a person records it or marks it to run again. There is no skip.
A type change that rewrites the table is not one ALTER COLUMN … TYPE. It is five migrations: add the new column, backfill it, dual-write, switch reads, then drop the old column. See type change.
Line endings
okm generate init writes .gitattributes in the project when that file is missing. An existing file stays as it is.
migrations/*.sql text eol=lf
*.catalog.json text eol=lf
.okm/** text eol=lfThe first line is the literal pattern migrations/*.sql.
A migration file is read as LF. \r\n and a lone \r become \n, including inside a SQL string. A literal that must contain a carriage return does not survive that read. A leading UTF-8 BOM is stripped.
UTF-16
A null byte is refused. The message is the file path followed by is UTF-16. Save the file as UTF-8.
Out
out is where okm build writes the catalog.
| Field | Required | Default | Role |
|---|---|---|---|
out | no | .okm | Build output. |
import { defineConfig } from "okmodel/migrate";
export default defineConfig({
schema: "./schema.ts",
out: "./.okm",
database: process.env.DATABASE_URL!,
});Omit out and the directory is .okm. The directory is created when it is missing. A relative path is resolved from the project directory. An absolute path stays absolute.
okm build writes catalog.json, catalog.hash, and types.d.ts. catalog.hash is the SHA-256 of catalog.json. types.d.ts is the row types. okm generate writes the same three files.
Timeouts
timeouts sets lock_timeout and statement_timeout for apply. Both values are milliseconds. A flag overrides the config for that invocation. 0 disables the timeout. Connect ceilings are a separate timeouts object.
| Field | Required | Default | Role |
|---|---|---|---|
lock | no | 5000 | lock_timeout when apply omits the field and the flag. --lock-timeout overrides it. |
statement | no | 30000 | statement_timeout when apply omits the field and the flag. --statement-timeout overrides it. |
import { defineConfig } from "okmodel/migrate";
export default defineConfig({
schema: "./schema.ts",
database: process.env.DATABASE_URL!,
timeouts: {
lock: 2000,
statement: 15000,
},
});--lock-timeout and --statement-timeout need a finite number of milliseconds that is not negative. A missing value fails the same way. The message is --lock-timeout needs a number of milliseconds. The same sentence names --statement-timeout.
commands
| Command | timeouts | Flag |
|---|---|---|
okm migrate apply | Used when the flag is omitted. | --lock-timeout, --statement-timeout. |
okm migrate check | Sets a field only when the config has it. Omit a field and that timeout stays unset. | Not applied. |
okm push, provision() | Not read. Both stay 5000 and 30000. | Not applied. |
okm check | Not read. The command does not set either timeout. | Not applied. |
okm check, okm push, and okm migrate check still parse the flag, so a bad number is refused. The value is not applied. provision() takes no flag.
session
Apply sets the two values on the session. okm migrate check sets them for its transaction.
A backfill batch sets lock_timeout to the apply lock timeout. It sets statement_timeout to backfill.statementTimeoutMs when that field is set, and to the apply statement timeout otherwise.
Pooler
allowPooler skips the pooler-host refusal.
Apply holds an advisory lock on one session. A transaction-mode pooler can hand the next statement a different backend.
| Field | Required | Default | Role |
|---|---|---|---|
allowPooler | no | false | Skip the refusal. --allow-pooler does the same for that invocation. Either one is enough. |
import { defineConfig } from "okmodel/migrate";
export default defineConfig({
schema: "./schema.ts",
database: process.env.DATABASE_URL!,
allowPooler: true,
});The host is lowercased before the check. The message is Refusing pooler host <host>. Apply needs a direct connection. The code is invalid.
| Shape | Result |
|---|---|
Host contains pooler | Refused. |
Host contains pgbouncer | Refused. |
Port 6543 or 6432 | Refused. |
Query pgbouncer=true | Refused by the migrate guard. Migrate strips that parameter before the URL becomes a startup parameter; other pooler hints are not interpreted. |
A direct postgres:// URL | Allowed. The flag is not required. |
| Command | Pooler host |
|---|---|
okm migrate apply, okm migrate check, okm push, okm seed | Refused until allowPooler or --allow-pooler. |
okm check, when it compares one database | Refused until allowPooler or --allow-pooler. |
provision() | Refused until allowPooler in the config. The call takes no flag. |
okm migrate status, okm migrate plan, okm doctor, okm ext | Not refused. |
connect() | Not refused. |
OKM1854
Apply, push, seed, provision, and a one-database okm check read pg_backend_pid() eight times on the reserved session. The probe does not hold another backend between reads. okm migrate check reads pg_backend_pid() eight times inside the scratch replay and does not hold another backend between those reads either. The reads are not one transaction. Two or more distinct pids are OKM1854. The same opt-in that skips the host check skips the probe.
Connection pool size is max.
Lint from
lintFrom is the first migration id okm migrate check lints. Files earlier in apply order are not linted. The id itself is linted.
Lint reads each SQL file in the migrations directory and reports findings. lintFrom chooses the first id in that pass.
| Value | Result |
|---|---|
| omitted | Every file. |
| a migration id | That file and every file after it, in apply order. |
| an id that is not a migration | Refused: lintFrom <id> is not a migration id. |
| an error finding on a linted file | OKM1510. |
| warnings only | The command continues. |
omitted
Leave lintFrom out. okm migrate check lints every file.
id
The id is the file name without .sql. okm generate writes a number of at least four digits, _, and the name. An empty directory and the name init produce 0001_init.
import { defineConfig } from "okmodel/migrate";
export default defineConfig({
schema: "./schema.ts",
database: process.env.DATABASE_URL!,
lintFrom: "0003_restore",
});0003_restore and every later file are linted. Earlier files are not. This skips lint only. The previous-catalog check, the head check, and the replay still cover every file.
other commands
okm migrate apply does not read lintFrom. It lints every migration that still has a step to run, before any DDL. A database with no okm_history lints every file. No other command reads lintFrom.
OKM1510
An error finding on a linted file fails okm migrate check. The message is one line per finding.
Backfill
backfill sets batch size, pause, and batch statement timeout.
batchSize is written into a new plan. pauseMs and statementTimeoutMs are read when the batches run.
| Field | Required | Default | Role |
|---|---|---|---|
batchSize | no | 1000 | Rows in batch= on a new plan. |
pauseMs | no | 0 | Milliseconds after a committed batch, and not after the last one. |
statementTimeoutMs | no | statement timeout, then 30000 | statement_timeout for one batch. |
import { defineConfig } from "okmodel/migrate";
export default defineConfig({
schema: "./schema.ts",
database: process.env.DATABASE_URL!,
backfill: {
batchSize: 1000,
pauseMs: 50,
statementTimeoutMs: 30_000,
},
});A new plan writes one header. It names the table, the key, and batch=. pauseMs and statementTimeoutMs stay out of the file.
-- backfill table="public"."notes" key="id" batch=1000Apply reads batch= from that line. Changing batchSize does not change a file that already exists.
Omit statementTimeoutMs and the batch uses the apply statement timeout, then 30 seconds. The batch lock_timeout is the apply lock timeout.
Refused
batchSize below 1, or not an integer, is refused: backfill.batchSize must be an integer of at least 1. A header whose batch is below 1 is refused.
Namespace
namespace is the Postgres schema this catalog owns. Omitted means public.
schema() still compiles public. okm places the catalog into namespace before it plans. Apply creates that schema when it is not public, then sets the session search path so okm_meta, okm_history, and okm_backfill land there. connect() reads okm_meta in that namespace. public keeps the unqualified query.
import { defineConfig } from "okmodel/migrate";
export default defineConfig({
schema: "./schema.ts",
database: process.env.DATABASE_URL!,
namespace: "identity",
});Two catalogs on one database use two configs and two values, for example identity and billing. The page for that layout is Catalogs.
External
external is a list of snapshot paths, one file per owning catalog. Omitted means this project owns every table it declares.
Each path is what okm catalog export wrote. okm check and okm migrate check compare every external() table with those files. A missing file, a table the files do not list, or columns that differ is OKM1841. On PostgreSQL 18 an external column that is NOT NULL … NOT VALID on the database is nullable for this comparison, and the message names the constraint. When a target is selected, the same commands also compare the file with that database.
import { defineConfig } from "okmodel/migrate";
export default defineConfig({
schema: "./schema.ts",
database: process.env.DATABASE_URL!,
namespace: "billing",
external: ["../identity/identity.okm.json"],
});The declaration is Catalogs.