Why Turbine

The argument for choosing Turbine, written so you can check it rather than take it.

First, what is not a reason#

Resolving a nested with clause in one statement is table stakes in 2026. Drizzle has compiled relational queries to LEFT JOIN LATERAL plus JSON aggregation since 0.28. Prisma does it under its relationJoins preview flag. Kysely ships jsonArrayFrom / jsonObjectFrom helpers. Turbine does it too, with correlated json_agg subqueries, and does it well, but anyone selling "no N+1" as the differentiator is describing the floor.

Neither is raw query speed on its own. Turbine is ahead on most of the benchmark suite and behind on streaming, and the four engines are close enough on a well-indexed schema that per-query overhead should not decide your choice alone.

One dependency, and no compiler on your event loop#

dependencies is one line: pg. No engine binary, no WASM module, no adapter packages in lockstep. Turbine is written from scratch: query compilation is plain string building with an FNV-1a shape fingerprint into a bounded LRU of SQL templates. The artifact is a parameterized SQL string, so there is no plan cache to size and no compiler competing with your request handlers.

That last part is a structural difference, not a benchmark result. Prisma 7 replaced its Rust engine with a WebAssembly query compiler that runs on the JavaScript main thread, and Prisma's own changelog records the cost: an LRU query-plan cache in 7.4.0 to avoid recompiling repeated shapes, then queryPlanCacheMaxSize in 7.8.0 to tune it. Turbine has nothing to tune there, because there is nothing running.

The footprint follows from the same decision: the main entry's import graph is held under a CI-enforced size-limit ceiling with pg external (run npm run size for the current figure), which is why the edge story is one import swap rather than a separate build.

Fast enough to publish the control arm#

The benchmark suite runs Turbine against Prisma, Drizzle, and a hand-written pg control on the same schema, data, and pool. In the last published run Turbine ran at 1.08x the raw pg control by geometric mean, where Drizzle ran at 1.47x and Prisma at 1.81x. Turbine took nine of ten scenarios; the tenth, streaming, was Drizzle's in that run and has reversed in both re-measurements since.

The losses are on the page next to the wins, with the measurement noise floor, because a benchmark you cannot audit is an advertisement.

Built for agents, safely#

Prisma ships an official MCP server and Drizzle ships drizzle-kit mcp, so "has an MCP server" is not the claim. The claim is what Turbine's server lets an agent do, and what it makes impossible:

  • Read-only by construction. All eleven tools run inside BEGIN READ ONLY with a statement timeout. No tool accepts SQL text, so there is no injection surface to harden and nothing to talk the agent into.
  • The schema questions agents actually struggle with are first-class. relation_graph returns the exact relation names a with clause accepts. find_join_path answers "how do I get from comments to orgs" with the with clause to write, as code. table_stats lets the agent check size before writing a query that would scan the table.
  • PII never reaches the model. sample_rows never fetches PII-tagged columns, and explain_query refuses predicates on them, because a planner row estimate can leak whether a value exists.

The rest of the agent story is structural: typed query args make a wrong query a compile error the agent can read, and stable error codes (TURBINE_E001..E018, each with a docs URL) give it something to branch on. Details and setup: Turbine for AI agents.

The operational tooling is in the box, offline#

turbine doctor. Turbine loads relations as correlated subqueries, so an unindexed FK becomes a scan per parent row. doctor derives every column set the relation queries probe, reports the ones with no covering index with a cost tier, and --fix writes the migration. It also detects cached-plan divergence, verified with a plan-only EXPLAIN rather than predicted. No cloud service, no telemetry, no account.

The checkable version of the claim, as of July 2026: no TypeScript ORM CLI offers missing-index advice. Not the Prisma CLI, not drizzle-kit, not kysely-ctl, not the MikroORM / TypeORM / Sequelize CLIs. Prisma Optimize was retired in March 2026 in favour of cloud-only Query Insights. Prior art exists outside TypeScript (Ruby's active_record_doctor), so the claim is "no TypeScript ORM", not "no ORM". The most direct outcome on record: one missing FK index found by doctor took a correlated plan from 17.8 s to 62 ms.

PII enforced in the emitted SQL. Tag a column pii: true and it is excluded from every default projection in the statement itself: RETURNING "id", "name" rather than RETURNING *, so the value never leaves the database. It is also refused as a groupBy key and a _min / _max target. Unlocking it takes includePii: UNSAFE, a symbol, so an options object built from a request body cannot escalate. A schema with no tagged column emits byte-identical SQL. Relatedly, errors carry keys and never values: a NotFoundError says where: { id, email } without printing the email.

A read-only Studio. npx turbine studio binds loopback, authenticates with a per-process token, runs every read inside BEGIN READ ONLY, and has no raw-SQL surface. Without --write the write endpoints do not exist in the router, so there is nothing to bypass. As of July 2026, no other TypeScript ORM ships a studio that is read-only by default or that redacts PII: Prisma Studio has no read-only mode (the request has been open since February 2021), Drizzle Studio is closed source with self-hosting through Drizzle Gateway, and TypeORM, MikroORM, Kysely and Sequelize ship no studio.

Destructive migrations need consent. migrate up, migrate down and push scan for data-destroying statements, print an itemized report, and refuse. Interactively you type destroy my data, then yes; in CI you pass --allow-destructive. A refused batch applies nothing.

Free, and forkable#

MIT, no cloud tier, no telemetry, no account: Studio, doctor, the MCP server, and observability are all in the npm package. Compare Prisma's index advice moving to a cloud subscription.

And it is built to be extended rather than wrapped. All SQL generation routes through the documented Dialect contract; the SQLite, MySQL, and SQL Server engines are implementations of that seam, not forks of the core. An engine Turbine does not ship starts there.

What Turbine is not#

Stated so you do not find out three weeks in:

  • Postgres is the primary target. SQLite, MySQL and SQL Server are real, tested engines, but pgvector, LISTEN/NOTIFY, and RLS sessionContext are Postgres-only and throw a typed UnsupportedFeatureError rather than degrading silently. The turbine generate / turbine migrate CLI is Postgres-only.
  • Only Postgres streams with a true cursor. The other engines' findManyStream materializes the result and then yields it in batches. Same rows, same API, not constant memory.
  • Streaming is the closest scenario, and the per-row API still loses it. Turbine's cursor buys arbitrary orderBy, early break, and nested with per batch, and on a full-table drain of a monotonic key that costs: findManyStream runs 5-11% behind Drizzle 1.0.0-rc.4. findManyStreamBatches gives up the per-row API to match the competitors' unit of work and draws level with rc.4 (inside the noise floor, both directions across three runs), passing Drizzle 0.45.2 by 20%. If a full-table drain on an ascending unique key is your hot path, measure it yourself.
  • It is younger. Fewer Stack Overflow answers, a smaller community. The mitigation is that the migration paths are real and reversible, not that the gap does not exist.
  • The schema is code-first or introspected. There is no .prisma DSL, which is a feature for some teams and a cost for others.

Coming from somewhere else#

  • Migrate from Prisma: turbine migrate-from-prisma reads your schema.prisma and emits the mapping, and a runtime compat adapter keeps PrismaClient-shaped call sites working while you port.
  • Migrate from Drizzle: the API mapping and the behavioural differences worth auditing.

Then check it yourself#

npx turbine doctor          # the index advice, on your own schema
npx turbine studio          # read-only by default, no flag needed
npx turbine studio --demo   # or with no database at all
npx turbine mcp             # the agent server, read-only