Operate

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: true on the production target. okm push is 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 missing okm_meta is 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, until okm migrate apply. The startup comparison is Startup check. The connect call is 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. The cases are on 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; named statements are on Drivers.
  • Run okm 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.

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.

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

export default defineConfig({
  schema: "./schema.ts",
  targets: {
    production: { url: process.env.DATABASE_URL, protected: true },
  },
});
schema.ts
import { schema, table, t } from "okmodel/pg";

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

export default schema({ tables: [notes] });
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:

apply.sh
okm migrate apply --target production

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

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

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.

The package copy is docs/production.md on main.

NextYou use it for
TestingFactories, query counts, and a tenant check on a real database.
Protected targetsThe fingerprint store and the agent roles.
doctor --productionEvery finding id.