Production checklist
Protect the target, require the catalog, and point apply at a direct connection.
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: trueon the production target.okm pushis refused only when the target is protected. The flag and the user store are on Protected targets. - Open the app with
connect({ requireMeta: true }). A missingokm_metais then 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, untilokm migrate apply. The startup comparison is Startup check. The connect call is Connect. - Pass
--targetwhenever 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-databaseokm check. Omitting it then is OKM1853. The cases are on Database. - Point
okm migrate apply,okm seed, and a connectingokm checkat a direct connection, not a pooler. A host containingpooler,pgbouncer, or-pooler, port6543or6432, or?pgbouncer=trueis refused unless--allow-pooleris set. Leavepreparedunset, or setprepared: "unnamed", behind a transaction-mode pooler. The host patterns are on Pooler; named statements are on Drivers. - Run
okm doctor --productionagainst that target and clear everyfailfinding 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.
After connecting, apply, seed, and a connecting check probe pg_backend_pid() across eight reads on the apply session (Database). Two or more distinct pids are 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.
import { defineConfig } from "okmodel/migrate";
export default defineConfig({
schema: "./schema.ts",
targets: {
production: { url: process.env.DATABASE_URL, protected: true },
},
});import { schema, table, t } from "okmodel/pg";
const notes = table("notes", { id: t.identity(), title: t.text() });
export default schema({ tables: [notes] });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:
okm migrate apply --target productionLock and statement timeouts, the apply summary, and a backfill progress line are on migrate apply and Timeouts.
Read the doctor report
okm 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.
okm doctor --production --target productionA 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) 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.
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.
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 okThe 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.
The package copy is docs/production.md on main.
| Next | You use it for |
|---|---|
| Testing | Factories, query counts, and a tenant check on a real database. |
| Protected targets | The fingerprint store and the agent roles. |
| doctor --production | Every finding id. |