# Production checklist

Nothing about an environment is inferred from `NODE_ENV` or from the target's name. A target named `production` is not protected by that name. Work through this list before the app serves production traffic.

- [ ] Set `protected: true` on the production target. [`okm push`](/docs/reference/cli/migrate#push) is refused only when the target is protected. The flag and the user store are on [Protected targets](/docs/operate/protected-targets-and-roles).
- [ ] Open the app with `connect({ requireMeta: true })`. A missing `okm_meta` is then [OKM1520](/docs/reference/errors#okm1520). The default is off so an existing database can adopt OKModel. A database whose catalog is behind the app is also OKM1520, with or without that option, until [`okm migrate apply`](/docs/reference/cli/migrate#migrate-apply). The startup comparison is [Startup check](/docs/operate/startup-check). The connect call is [Connect](/docs/operate/connect).
- [ ] Pass `--target` whenever more than one target is configured and the command must pick one database: `okm migrate apply`, `okm push`, `okm seed`, `okm migrate check`, and a one-database `okm check`. Omitting it then is [OKM1853](/docs/reference/errors#okm1853). The cases are on [Database](/docs/reference/config-database).
- [ ] Point `okm migrate apply`, `okm seed`, and a connecting `okm check` at a direct connection, not a pooler. A host containing `pooler`, `pgbouncer`, or `-pooler`, port `6543` or `6432`, or `?pgbouncer=true` is refused unless `--allow-pooler` is set. Leave `prepared` unset, or set `prepared: "unnamed"`, behind a transaction-mode pooler. The host patterns are on [Pooler](/docs/reference/config#pooler); named statements are on [Drivers](/docs/operate/drivers).
- [ ] Run [`okm doctor --production`](/docs/reference/cli/check#doctor---production) against that target and clear every `fail` finding before go-live.

`okm migrate status` lists every target and does not require `--target`. It stops at the first unreachable one.

`okm check` with several targets and no `--target` does not connect. It prints `server checks skipped: N targets, pass --target <name>`, then the lint text and `ok`. One target, or `--target`, does connect. That command is [check](/docs/reference/cli/check#check).

After connecting, apply, seed, and a connecting check probe `pg_backend_pid()` across eight reads on the apply session ([Database](/docs/reference/config-database)). Two or more distinct pids are [OKM1854](/docs/reference/errors#okm1854). That probe catches a transaction-mode pooler that rotates backends between statements (`server_round_robin=1`). Default LIFO PgBouncer (`server_round_robin=0`) on a non-heuristic port can keep one pid for all eight reads, so mark it with `?pgbouncer=true` (or put `pooler` or `pgbouncer` in the host) instead of relying on the probe. Warm-and-hold with a second connection is not used during apply or seed: it deadlocks single-session targets. `--allow-pooler` skips the URL check and the probe. Apply wants a direct connection.

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

export default defineConfig({
  schema: "./schema.ts",
  targets: {
    production: { url: process.env.DATABASE_URL, protected: true },
  },
});
```

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

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

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

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

const notes = table("notes", { id: t.identity(), title: t.text() });
const app = schema({ tables: [notes] });
const url = process.env.DATABASE_URL;
if (url === undefined || url.length === 0) throw new Error("DATABASE_URL is not set");

const db = await connect(url, {
  schema: app,
  requireMeta: true,
});
await db.close();
```

Apply that target by name:

```sh title="apply.sh"
okm migrate apply --target production
```

Lock and statement timeouts, the apply summary, and a backfill progress line are on [migrate apply](/docs/reference/cli/migrate#migrate-apply) and [Timeouts](/docs/reference/config#timeouts).

## Read the doctor report

[`okm doctor --production`](/docs/reference/cli/check#doctor---production) reads one target and reports how it differs from the setup above. It is read-only: every query runs in one `READ ONLY` transaction on one fresh session, and the command makes no network call beyond the target database. It needs a target. With several targets and no `--target` it is OKM1853 and exit 2, not a skip. It takes no error code, and `--bundle` is a different command. `--allow-pooler` on this command changes no connection. It tells the check that apply is allowed through a pooler, which turns a pooler URL from a warning into a fail.

```sh title="doctor.sh"
okm doctor --production --target production
```

A finding has a stable id, a severity (`fail`, `warn`, or `ok`), and a one-line fix. The command exits 1 when any finding is `fail` ([OKM2204](/docs/reference/errors#okm2204)) and 0 otherwise. `--json` prints the `okm.cli/1` envelope with `data.production`. The warn and fail findings are also in `warnings`. The ids and the rule for each one are on [doctor --production](/docs/reference/cli/check#doctor---production).

The output names the target as ``target `production` ``. It never prints the URL, host, user, or password. A failed connection says ``Could not read target `<name>` `` and exits 5. When the driver message contains `ECONNREFUSED`, `ENOTFOUND`, `ETIMEDOUT`, `ECONNRESET`, `EHOSTUNREACH`, `28P01`, `28000`, `3D000`, or `57P03`, that token is appended in parentheses.

```text title="doctor-production.out"
target `production`
ok    target-protection           The target sets protected: true.
warn  pooler             OKM2203  The target URL looks like a pooler (the host name). okm migrate apply refuses it.
                                  fix: Point the target at a direct connection.
ok    okm-meta                    okm_meta exists. The check cannot see connect() options: confirm the app passes requireMeta: true.
ok    role-privilege              The login role is not a superuser and does not have BYPASSRLS.
warn  role-owns-tables   OKM1707  The login role owns 3 of the schema's tables. That is fine for the migrator and wrong for the app role: an owner is not bound by row-level security.
                                  fix: Run the app as a role that does not own the tables (docs/roles.md).
ok    lock-timeout                okm migrate apply sets lock_timeout to 5000 ms (default) and that value applies to migrations. The role's session value is 0 (no limit) and applies to everything else.
warn  statement-timeout  OKM2201  statement_timeout is 0 for this role in a fresh session, so a statement runs without limit. okm migrate apply sets 30000 ms (default) for migrations only.
                                  fix: Run ALTER ROLE <app role> SET statement_timeout = '30s' (or the limit you want).
ok    server-version              The server is PostgreSQL 17, inside the supported window 15-18.
ok    extensions                  The schema requires no extension.
0 fail, 3 warn, 6 ok
```

The doctor cannot see `connect()` options. `requireMeta: true` is an application option, so a present `okm_meta` is reported as `ok` with a reminder, and a missing one is a fail because `requireMeta` would refuse that database (OKM1520). It also cannot tell whether a login is the migrator or the app role: the role finding says what the login role can do, and you read it against the role you meant. A `--lock-timeout` or `--statement-timeout` on one `okm migrate apply` run overrides the apply value the doctor reports. The check does not report whether an exposed table has validation off: the catalog does not record `validation`, so there is no finding for it. The role recipe is [Protected targets](/docs/operate/protected-targets-and-roles).

The package copy is [`docs/production.md`](https://github.com/omqkhafi/okmodel/blob/main/docs/production.md) on main.

| Next | You use it for |
| --- | --- |
| [Testing](/docs/operate/testing) | Factories, query counts, and a tenant check on a real database. |
| [Protected targets](/docs/operate/protected-targets-and-roles) | The fingerprint store and the agent roles. |
| [doctor --production](/docs/reference/cli/check#doctor---production) | Every finding id. |

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