# How it works

You write each table once, in a `schema()` call. The client types, the SQL each query sends, the migration files, and the check that the live database still matches all come from that one object, so they cannot disagree.

A new reader opens [Installation](/docs/start/installation) first, then [First schema](/docs/start/first-schema).

## The problem

When a table is described in three places, the copies drift apart. The TypeScript type names one column, a query sends another, and a migration creates a third, and nothing in the project notices until a request fails.

## One table through the flow

This section follows one `notes` table from the schema to a database that has changed by hand. Every output below comes from running these files.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity().primaryKey(),
  title: t.text(),
});

export default schema({ tables: [notes] });
```

`schema()` compiles the tables into a catalog: one document that names every table, column, and constraint. Everything below reads that catalog.

### The client

`connect` takes the schema and returns `db`, with one typed property per table.

```ts title="find.ts"
import { connect } from "okmodel/pg/postgresjs";

import schema from "./schema.ts";

export async function sample(url: string) {
  await using db = connect(url, { schema });
  return await db.notes.find({ where: { id: "1" }, limit: 1 });
}
```

```text title="find.out"
[
  {
    id: "1",
    title: "Hello",
  }
]
```

### The types

`okm` is the command-line tool that ships with the package. It reads `okmodel.config.ts`, which names the schema file and the database. That database is the default target: the database an `okm` command runs against when you do not pass `--target`.

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

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

`bunx okm generate init` writes the first migration and, beside it, `.okm/types.d.ts` with the row type, the insert type, and the update type of each table. The `id` is generated by the database, so the insert type has no `id`, and every update field is optional:

```ts title=".okm/types.d.ts"
/**
 * Row types emitted from the schema.
 * Mode: emitted.
 */

export interface Notes {
  readonly id: string;
  readonly title: string;
}

export interface NotesInsert {
  readonly title: string;
}

export interface NotesUpdate {
  readonly title?: string;
}
```

The client reads the same row shapes from the schema value you pass to `connect`, so an app does not need to import this file. [Types](/docs/concepts/type-safety-and-inference) explains the `types` setting and the file.

### The migration

`okm generate init` compares the catalog with an empty snapshot (a saved copy of the catalog from the last migration) and writes the SQL that turns one into the other:

```sql title="0001_init.sql"
-- class: expand
-- name: init

-- class: expand
-- kind: create-table
-- action: ddl
-- lock: ACCESS EXCLUSIVE
create table "public"."notes" (
  "id" bigint generated always as identity not null,
  "title" text not null,
  constraint "notes_pkey" primary key ("id")
);
```

`bunx okm migrate apply` runs it on the default target:

```text title="apply.out"
target default
applied provisioned@0001_init
```

### The check

Drift is a difference between the live database and the schema, such as a column someone added by hand. `bunx okm check` compares the two and prints `ok` when they match:

```text title="check.out"
ok
```

After `alter table notes add column body text` is run by hand, the same command exits with status 1 and prints the drift as [OKM1520](/docs/reference/errors#okm1520), with the statement that would remove it:

```text title="check-drift.err"
error OKM1520: The database differs from the schema.
alter table "public"."notes" drop column "body"
Change the schema or the database so okm check reports no difference.
```

Fix it in the schema, by adding the column and generating a migration, or in the database, by dropping the column. `okm check` never changes the database.

| Step | What runs |
| --- | --- |
| `schema()` | Builds the catalog and the table types. |
| `connect(url, { schema })` | Opens `db`, typed from that schema. |
| `db.notes.find({ … })` | Builds the SQL when you call it and returns rows. |
| `okm generate <name>` | Writes migration SQL from the last snapshot to this catalog. |
| `okm migrate apply` | Runs pending migrations on the target. |
| `okm check` | Reports drift between the target and the schema. |

## How the pieces split

The schema is the model. `connect` is the runtime: it opens `db` in your app and needs only the schema and a URL. `defineConfig` is the tooling: it tells `okm` where the schema is, which targets exist, and which roles to create. The app does not import the config.

| The schema gives you | Page |
| --- | --- |
| Types for the client, and for rows, inserts, and updates | [Types](/docs/concepts/type-safety-and-inference) |
| Queries on each table | [Queries](/docs/queries) |
| Migration SQL from one catalog to the next | [Drift](/docs/concepts/migrations-and-drift) |
| The live `okm check` comparison | [Drift](/docs/concepts/migrations-and-drift) |
| Check constraints | [Checks](/docs/schema/checks) |

`okm check` compares a reachable database to the schema ([check](/docs/reference/cli/check#check)). Replaying the migration files into a scratch schema is a different command, `okm migrate check` ([Lint](/docs/migrations/checks-and-lint)). There is no separate `okm lint` command: lint findings appear in plan, apply, and those two check commands.

## Limits and trade-offs

The catalog hash, a SHA-256 of the catalog document, does not depend on table order. The emitted interfaces do. Swapping two tables leaves the hash the same and changes the order of those interfaces. [The schema](/docs/schema/the-schema) explains why.

Roles and grants are not part of `schema()`. When the config names `roles`, the tooling adds them before a plan or a check. `connect` hashes the schema catalog without them.

## Where to read the detail

| Page | You use it for |
| --- | --- |
| [Catalog](/docs/concepts/the-schema-is-the-catalog) | What `schema().catalog` means for generate, plan, check, and the client. |
| [The schema](/docs/schema/the-schema) | The declaration and the tables list. |
| [First schema](/docs/start/first-schema) | One table, then insert and read. |
| [First migration](/docs/start/first-migration) | Generate the first SQL file and apply it. |

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