# Config

<Anatomy call="defineConfig" shape="object" exported>
  <AnatomyPart slot="schema" label="Schema" code='schema: "./schema.ts",' mark="required">
    [`schema`](#schema) is required. `okm` loads this file.
  </AnatomyPart>
  <AnatomyPart slot="database" label="Database" code="database: process.env.DATABASE_URL!," mark="or targets">
    [`database`](/docs/reference/config-database) is one target named `default`. `targets` replaces it. Set `database` or `targets`. Both is refused.
  </AnatomyPart>
  <AnatomyPart slot="sections" label="Options" code="…," mark="options">
    Each other field is a heading below, or [Roles](/docs/reference/config-roles).
  </AnatomyPart>
</Anatomy>

`defineConfig` is the file `okm` reads. `okm` reads `okmodel.config.ts` from the project directory. The file default-exports `defineConfig` from `okmodel/migrate`.

<Callout type="error" title="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(...).`
</Callout>

<Callout type="info" title="Runtime">
Node.js 22.18 or newer loads the config. `bunx okm` uses Node. `bun --bun okm` uses Bun.
</Callout>

## The declaration

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({ // [!code focus]
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
});
```

| Order | Option | You use it for |
| --- | --- | --- |
| 1 | [Schema](#schema) | `schema` is required. `okm` loads this file. |
| 2 | [Database](/docs/reference/config-database) | `database` is one target named `default`. `targets` replaces it. |
| 3 | [Tables](#tables) | `tables` is a directory `okm check` reads for orphan table files. |
| 4 | [Migrations](#migrations) | `migrations` is where `okm generate` writes SQL. |
| 5 | [Out](#out) | `out` is where `okm build` writes the catalog. |
| 6 | [Timeouts](#timeouts) | `timeouts` sets `lock_timeout` and `statement_timeout` for apply. |
| 7 | [Pooler](#pooler) | `allowPooler` skips the pooler-host refusal. |
| 8 | [Lint from](#lint-from) | `lintFrom` is the first migration id `okm migrate check` lints. |
| 9 | [Backfill](#backfill) | `backfill` sets batch size, pause, and batch statement timeout. |
| 10 | [Roles](/docs/reference/config-roles) | `roles` names migration, app, and optional reporting grants. |
| 11 | [Namespace](#namespace) | `namespace` is the Postgres schema this catalog owns. |
| 12 | [External](#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(...).`

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts", // [!code focus]
  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()`](/docs/schema/the-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](/docs/schema/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.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  tables: "./tables", // [!code focus]
  database: process.env.DATABASE_URL!,
});
```

<Callout type="error" title="OKM1024">
`Table file not in schema: extra.ts.` Add the file's table to the `schema()` call that `okm check` reads.
</Callout>

## 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.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  migrations: "migrations", // [!code focus]
});
```

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`](#lint-from).

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](/docs/reference/type-change).

### Line endings

`okm generate init` writes `.gitattributes` in the project when that file is missing. An existing file stays as it is.

```text
migrations/*.sql text eol=lf
*.catalog.json text eol=lf
.okm/** text eol=lf
```

The 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.

<Callout type="error" title="UTF-16">
A null byte is refused. The message is the file path followed by `is UTF-16. Save the file as UTF-8.`
</Callout>

## Out

`out` is where `okm build` writes the catalog.

| Field | Required | Default | Role |
| --- | --- | --- | --- |
| `out` | no | `.okm` | Build output. |

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  out: "./.okm", // [!code focus]
  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](/docs/operate/session-and-pool#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. |

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
  timeouts: {
    lock: 2000, // [!code focus]
    statement: 15000, // [!code focus]
  },
});
```

`--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. |

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
  allowPooler: true, // [!code focus]
});
```

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. |

<Callout type="warning" title="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.
</Callout>

Connection pool size is [`max`](/docs/operate/session-and-pool#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](#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`.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
  lintFrom: "0003_restore", // [!code focus]
});
```

`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`.

<Callout type="error" title="OKM1510">
An error finding on a linted file fails `okm migrate check`. The message is one line per finding.
</Callout>

## 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. |

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
  backfill: { // [!code focus]
    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.

```sql title="migrations/20260301000000_init.sql"
-- backfill table="public"."notes" key="id" batch=1000
```

Apply reads `batch=` from that line. Changing `batchSize` does not change a file that already exists.

Omit `statementTimeoutMs` and the batch uses the apply [statement](#timeouts) timeout, then 30 seconds. The batch `lock_timeout` is the apply lock timeout.

<Callout type="error" title="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.
</Callout>

## 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.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
  namespace: "identity", // [!code focus]
});
```

Two catalogs on one database use two configs and two values, for example `identity` and `billing`. The page for that layout is [Catalogs](/docs/schema/several-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.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
  namespace: "billing",
  external: ["../identity/identity.okm.json"], // [!code focus]
});
```

The declaration is [Catalogs](/docs/schema/several-catalogs).

Index: [/llms.txt](/llms.txt)
