Turbine

CLI

The turbine CLI ships with turbine-orm. Run npx turbine <command> from your project root.

Command index#

npx turbine <command> [options]
 
Commands:
  init                     Initialize a Turbine project
  generate | pull          Introspect the database, generate the typed client
  push                     Apply defineSchema() output to the database
  migrate create <name>    Create a SQL migration file (--auto | --from-diff | --recipe)
  migrate up               Apply pending migrations
  migrate deploy           Apply pending migrations without prompts (CI)
  migrate down             Roll back the last migration (or the last N)
  migrate status           Show applied vs pending migrations
  seed                     Run the seed file (.ts / .js / .sql)
  status | info            Show the introspected schema
  doctor                   Missing-index triage + cached-plan divergence
  migrate-from-prisma      Map a schema.prisma onto Turbine (report + typed name map)
  studio                   Launch the local Studio web UI
  mcp                      Start the read-only MCP server over stdio
  observe                  Launch the local metrics dashboard
  skill                    Install the agent query skill

Every command takes --help. An unknown flag stops the command and names the closest valid one.

Global options#

FlagDescription
--url, -u <url>Connection string. Overrides DATABASE_URL and the config file.
--out, -o <dir>Generated-code directory (default ./generated/turbine).
--schema, -s <name>Postgres schema name (default public).
--include <a,b> / --exclude <a,b>Limit the tables a command sees.
--verbose, -vDetailed logging.

turbine init#

Bootstraps a project, running only the steps that are missing: write turbine.config.ts, scaffold ./turbine/schema.ts and ./turbine/seed.ts, then, when a database is reachable, offer to push, generate and seed.

npx turbine init
npx turbine init --url postgres://user:pass@localhost:5432/mydb --schema app
FlagDescription
--yes, -yAccept every default without prompting.
--force, -fOverwrite an existing turbine.config.ts.
--schema-file <path>Where to scaffold the schema file (default ./turbine/schema.ts).
--skip-schema / --with-schemaSkip the schema file, or scaffold it even when the database already has tables.
--skip-seed, --skip-push, --skip-generateSkip that step.
  • Re-running is safe. Without a TTY, init scaffolds and generates, and skips push and seed.
  • A --url with a password goes to .env (added to .gitignore), and the config reads process.env.DATABASE_URL.
  • --schema <name> is written into the config, so later commands target the same namespace.

turbine pull / turbine generate#

Introspects the live database and writes a typed client. pull is an alias.

npx turbine generate
npx turbine generate --out ./src/db --zod

It writes types.ts (entity and input types), metadata.ts (the runtime SchemaMetadata) and index.ts (a typed TurbineClient subclass and the turbine() factory).

FlagDescription
--schema-file <path>Your defineSchema() file, whose column tags are applied (default ./turbine/schema.ts).
--zodAlso emit zod.ts. Your project supplies zod. See Zod Schemas.
--include-viewsInclude views and materialized views as read-only entities. See Views.
--keep-column-namesUse raw column names (created_at) as field names. Config: keepColumnNames.
--no-timestampOmit the Generated at header, for byte-identical output.
--import-ext <mode>Extension on imports between generated files: js, ts, none or auto (default). Config: importExtension.
--legacy-to-many-uniquesType a child with a unique foreign key as hasMany instead of hasOne. Config: legacyToManyUniques.
--allow-emptyGenerate even when no tables match.
  • Column tags. generate reads the pii, updatedAt, maxLength and tsType tags from your schema file, since the database cannot store them. A named file that is missing or fails to load stops the command; a missing default is skipped. See How generate picks up the tags.
  • --import-ext ts is for running the client with Node's type stripping (Node 22.18+ or 23.6+). Your type checker then needs allowImportingTsExtensions or rewriteRelativeImportExtensions.
  • Field collisions. Two columns that camelCase to one field ("createdAt" beside created_at) fail with TURBINE_E003. Use --keep-column-names or rename a column.

Typing columns, columnTypes#

A json / jsonb column generates as unknown. columnTypes in turbine.config.ts replaces the type of any column, keyed "<table>.<column>" by database names:

const config: TurbineCliConfig = {
  columnTypes: {
    'users.settings': '{ theme: string; beta?: boolean }',
    'orders.meta': "import('../src/types.js').OrderMeta",
  },
};

The text is emitted verbatim into the row, create and update types, so an import() resolves from the generated directory. It must be one type on one line. A nullable column still gets | null. An unknown key or malformed type throws TURBINE_E003 with the closest name. columnTypes wins over a schema file's tsType.

Renaming derived relations, relationNames#

Introspection names relations from foreign keys, so two foreign keys to one table give names nobody would guess:

messages.sender_id    -> people.msgsBySender
messages.recipient_id -> people.msgsByRecipient

Declare better names once in turbine.config.ts:

// turbine.config.ts
export default {
  relationNames: {
    people: {
      msgsBySender: 'sentMessages',
      msgsByRecipient: 'receivedMessages',
    },
  },
};

generate emits the new names in the metadata and types, for use in where, with and orderBy. An unknown table or relation, or a name that shadows a column, fails with TURBINE_E003 and lists the names that exist.

turbine push#

Applies your defineSchema() file to the database, in one transaction. Use it in development; use migrations in production.

npx turbine push
npx turbine push --dry-run --exit-code   # CI: exit 2 when the database differs
FlagDescription
--schema-file <path>Your defineSchema() file (default ./turbine/schema.ts).
--dry-runPrint the SQL without running it.
--exit-codeWith --dry-run: exit 2 when the database differs or the diff has a warning it cannot apply, 0 when in sync.
--allow-destructiveSkip the confirmation for data-destroying statements.

When the database already matches, push does nothing. It lists what the diff does not compare; see what --auto can and cannot do. Data-destroying statements hit the destructive guard.

turbine migrate create <name>#

Writes turbine/migrations/<timestamp>_<name>.sql with -- UP and -- DOWN sections.

npx turbine migrate create add_users_table              # blank
npx turbine migrate create add_email_index --auto       # from the schema diff
npx turbine migrate create sync_schema --from-diff      # diff, destructive statements flagged
npx turbine migrate create widen_total --recipe backfill
-- 20260409143022_add_email_index.sql
-- UP
CREATE UNIQUE INDEX "users_email_idx" ON "users" ("email");
 
-- DOWN
DROP INDEX "users_email_idx";
FlagDescription
--autoFill both sections from the diff between your schema file and the database.
--from-diffLike --auto, and flag each data-destroying statement inline and in a banner. Not combinable with --auto or --recipe.
--recipe <name>Scaffold a named pattern. backfill is the two-phase column type change.
--allow-pendingDiff even while earlier migrations are pending.
--schema-file <path>The schema file to diff.

--auto and --from-diff refuse to run while migrations are pending, because the diff would repeat what those files do.

turbine migrate up#

Applies pending migrations in timestamp order, each in its own transaction.

npx turbine migrate up
npx turbine migrate up --step 1 --dry-run
FlagDescription
--step, -n <N>Apply only the first N pending migrations.
--dry-runPrint the SQL without running it.
--allow-driftAccept edited applied files and re-record their checksums. Nothing re-runs.
--allow-destructiveSkip the confirmation for data-destroying statements.
--allow-poolerRun through a transaction pooler anyway.
  • The run holds pg_try_advisory_lock(), so a concurrent run fails with MigrationError (TURBINE_E006).
  • An applied file that was edited stops the run.
  • up, deploy and down refuse a pooler endpoint (a pooler or pgbouncer hostname, or port 6543 or 6432), because the lock is session-level. Use the direct endpoint.
  • A file with -- turbine:no-transaction runs without BEGIN / COMMIT, one statement at a time.

Destructive-migration guard#

migrate up, migrate down and push refuse to run SQL that destroys data:

  • DROP TABLE, DROP SCHEMA, DROP COLUMN, DROP VIEW ... CASCADE
  • TRUNCATE and DELETE FROM
  • an UPDATE with no WHERE, or one that is always true (WHERE 1=1)
  • ON CONFLICT DO UPDATE without a WHERE
  • lo_unlink
  • ALTER COLUMN ... TYPE, unless a -- turbine:widening marker declares it lossless
  • any of these inside a PREPARE ... AS body

Comments and string literals are ignored, and DROP INDEX, DROP CONSTRAINT and DROP TRIGGER pass. On a hit, nothing runs. Confirm interactively (type destroy my data, then yes) or pass --allow-destructive. Programmatic migrateUp / migrateDown take allowDestructive: true.

turbine migrate deploy#

Applies pending migrations without prompting, for CI and production.

npx turbine migrate deploy
npx turbine migrate deploy --dry-run    # list pending migrations
npx turbine migrate deploy --wait=120   # wait up to 120 s for another deploy's lock
FlagDescription
--dry-runList pending migrations and destructive statements without applying.
--wait[=<seconds>]Wait for a concurrent deploy's lock (default 300 s), then apply what is still pending.
--allow-driftDeploy over edited applied files, re-recording their checksums.
--allow-poolerRun through a transaction pooler anyway.

It uses the same lock as migrate up; without --wait, a held lock exits 1. A checksum mismatch or missing file also exits 1. The destructive guard does not run here: destructive statements print a NOTICE, then run. deploy never generates, seeds or pushes.

turbine migrate down#

Rolls back the most recent migration by running its -- DOWN section.

npx turbine migrate down --step 2

It takes --step, -n <N>, --dry-run, --allow-destructive, --allow-drift and --allow-pooler, as migrate up does. A destructive -- DOWN hits the same guard, and one with no SQL is reported with a warning.

turbine migrate status#

Shows which migrations are applied and which are pending.

npx turbine migrate status
npx turbine migrate status --exit-code   # CI: exit 2 on pending, modified, missing or misnamed files
i 2 applied, 1 pending
 
 Status    | Migration                          | Applied at
-----------|------------------------------------|-------------------------
 v Applied | 20260401120000_create_users.sql    | 2026-04-01 12:00:00 UTC
 v Applied | 20260402091234_add_posts.sql       | 2026-04-02 09:12:34 UTC
 . Pending | 20260409143022_add_email_index.sql |
 
  Run npx turbine migrate up to apply pending migrations.

An edited applied file shows as ! Drifted, a deleted one as ! Missing file.

turbine seed#

Runs the seed file.

npx turbine seed

The path is seedFile in turbine.config.ts (seed is an alias), else the first that exists of seed.ts, seed.js, seed.sql, turbine/seed.ts, turbine/seed.js, turbine/seed.sql. A .ts file runs with npx tsx, a .js file's default export is called, and a .sql file runs as raw SQL.

// seed.ts
import { defineSeed } from 'turbine-orm';
 
export default defineSeed(async (db) => {
  await db.raw`INSERT INTO orgs (name) VALUES (${'Acme'}) ON CONFLICT DO NOTHING`;
});

See Seeding.

turbine status#

Prints the introspected schema. info is an alias.

npx turbine status

It shows the connection string (password redacted), the schema and table count, then each table's columns, types, primary key and relations, and any enums.

turbine doctor#

Finds relations whose foreign key has no index, ranks each fix, writes the migration with --fix, and flags reads a cached plan could slow down. Every flag, the triage tiers and the JSON report are on its own page.

npx turbine doctor
npx turbine doctor --fix

turbine migrate-from-prisma#

Parses a schema.prisma, resolves each name against your database, and writes a report, the typed PRISMA_MAP the prisma-compat adapter runs on, and the generated client. It needs neither prisma nor @prisma/client.

npx turbine migrate-from-prisma --schema prisma/schema.prisma
npx turbine migrate-from-prisma --schema prisma/schema.prisma --no-db   # report only
FlagDescription
--schema <file>Path to schema.prisma (default prisma/schema.prisma). Here --schema is the Prisma file; the namespace is public.
--url, -u <url>Connection string. See Connection string.
--out, -o <dir>Output directory (default ./generated/turbine), inside the current directory.
--no-dbWrite the report only, without the map or client.
--allow-partialExit 0 when items are unresolved (default: exit 1).
--if-dbExit 0 without doing anything when no connection string resolves.
--no-timestampOmit the generated-at lines.

An unresolved item never blocks the client. A stale prisma-map.ts fails silently, so run the command in postinstall next to prisma generate, with --if-db; see Keeping the map current. Full workflow: Migrating from Prisma.

The --out directory guard#

migrate-from-prisma refuses to write outside the current working directory:

$ npx turbine migrate-from-prisma --out /tmp/turbine-scratch
Output directory must be within the project root. Got: /tmp/turbine-scratch
# exit code 1

A relative path that climbs out (../shared) is refused too. To write into a scratch directory, run the command from inside it with an absolute --schema. turbine generate has no such guard.

turbine studio#

Launches a local web UI with a visual findMany builder, a data browser and a schema view. It is read-only by default and redacts PII-tagged columns on the server.

npx turbine studio
npx turbine studio --demo   # seeded sample database, no DATABASE_URL

Flags: --port, --host, --no-open, --allow-remote, --write, --show-pii, --demo. Details and the security model: Studio.

turbine mcp#

Starts a read-only Model Context Protocol server over stdio, so an agent can inspect your schema, plans and sample rows. It takes the global flags only.

npx turbine mcp

Tools and client setup: MCP Server.

turbine skill#

Installs the query-writing skill an agent reads before writing Turbine queries.

npx turbine skill                  # -> .claude/skills/turbine-orm/SKILL.md
npx turbine skill --print          # to stdout
npx turbine skill --agents         # a short block for AGENTS.md / CLAUDE.md
npx turbine skill --dir .agent/skills

See Turbine for AI agents.

turbine observe#

Serves a local dashboard over the _turbine_metrics table that db.$observe() writes.

npx turbine observe --metrics-url postgres://...

Flags: --metrics-url (default TURBINE_OBSERVE_URL), --port (default 4984), --host, --no-open, --allow-remote. See Observability.

Config resolution#

The CLI loads the first of turbine.config.ts, turbine.config.mts, turbine.config.js and turbine.config.mjs in the current directory that imports successfully.

import type { TurbineCliConfig } from 'turbine-orm/cli';
 
const config: TurbineCliConfig = {
  url: process.env.DATABASE_URL,
  out: './generated/turbine',
  schema: 'public',
  migrationsDir: './turbine/migrations',
  seedFile: './turbine/seed.ts',
  schemaFile: './turbine/schema.ts',
  columnTypes: { 'users.settings': '{ theme: string }' },
};
 
export default config;

A flag beats the config file, which beats the default. migrationsDir, seedFile, columnTypes and relationNames come from the config file only. Every field: TurbineCliConfig.

Connection string#

A command that connects uses the first non-empty value of:

  1. --url
  2. DATABASE_URL
  3. url in turbine.config.ts
  4. migrate-from-prisma only: the schema's datasource block, through its env("...") (url, then directUrl) or a literal url

The CLI loads .env from the current directory (Node 20.12+), but a variable already set in your shell wins. observe reads --metrics-url, then TURBINE_OBSERVE_URL, never DATABASE_URL.

See also#