Turbine + BataDB
From zero to typed queries on BataDB, a Postgres-wire-compatible serverless platform with instant branching and scale-to-zero. Because BataDB speaks the Postgres wire, Turbine runs against it with zero glue, and the same typed query code works whether you connect over a long-lived TCP pool or over SQL-over-HTTP from the edge.
1. Install#
npm install turbine-orm # the ORM, one runtime dep (pg)
npm install @batadata/serverless # only for the edge / HTTP transport2. Generate your client#
Point Turbine at your direct BataDB endpoint and generate a fully-typed client. Grab the connection string from bata create … --json, the dashboard's Connect panel, or bata db url:
DATABASE_URL="postgresql://cloud_admin:…@cp-XXXX.us-east-1.db.batadata.com:5432/postgres?sslmode=require" \
npx turbine generateThis introspects your schema over the Postgres wire and writes typed accessors to ./generated/turbine/ (types.ts, metadata.ts, index.ts).
Generation reads the catalog over TCP, so the branch's compute must be running. bata generate wraps the same command; re-run it (or --watch) whenever your schema changes.
3. Connect (Node.js / long-running server)#
For traditional Node.js servers, Express, Fastify, or any worker or job, Turbine owns the pool and you get the full feature set: nested json_agg reads, transactions, streaming cursors, LISTEN/NOTIFY, pgvector, RLS.
import { turbine } from './generated/turbine';
const db = turbine({
connectionString: process.env.DATABASE_URL,
});
const authors = await db.authors.findMany({
where: { active: true },
with: { posts: { orderBy: { views: 'desc' }, limit: 5 } }, // one query, no N+1
limit: 20,
});The generated turbine() factory returns a typed TurbineClient subclass, every column, operator, and relation is autocompleted and inferred.
4. Connect (Serverless / Edge)#
For Vercel Edge, Cloudflare Workers, Deno Deploy, or any serverless runtime, use SQL-over-HTTP via @batadata/serverless. Its Pool is a PgCompatPool (conformant since 0.3.0), so turbineHttp(pool, SCHEMA) binds it with no adapter code:
import { Pool } from '@batadata/serverless';
import { turbineHttp } from 'turbine-orm/serverless';
import { SCHEMA } from './generated/turbine/metadata';
const pool = new Pool({
apiKey: process.env.BATA_API_KEY, // bata_…, routes + authorizes the request
projectId: process.env.BATA_PROJECT_ID,
branchId: process.env.BATA_BRANCH_ID, // optional, defaults to the primary branch
});
const db = turbineHttp(pool, SCHEMA);
// Same API, including the nested json_agg read, works unchanged over HTTP:
const authors = await db.authors.findMany({
where: { name: 'Ada Lovelace' },
with: { posts: true },
});
// authors[0].posts[0].title ← fully typed5. Migrations#
SQL-first migrations, no shadow database and no proprietary schema DSL:
# Create a migration
npx turbine migrate create add_authors_table
# Apply migrations
DATABASE_URL="…" npx turbine migrate up
# Check status
npx turbine migrate statusPooled vs. direct endpoint, SSL#
…db.batadata.com, direct endpoint, one connection per client. Use it forturbine generateand for the Node.js / worker path above.…-pooler.db.batadata.com, pooled endpoint (PgBouncer). Use it for many short-lived connections or serverless that bypasses the HTTP driver.- Always use TLS.
sslmode=requireis the minimum; pinsslmode=verify-full(with a CA) for the strongest guarantee, oruselibpqcompat=true&sslmode=requirefor libpq parity. Recentpgversions warn thatrequirewill adoptverify-fullsemantics in a future major.
Caveats over HTTP#
SQL-over-HTTP is stateless, one transaction per request, so anything that needs a held session is unavailable on the HTTP transport. Turbine fails loud with a typed error rather than running non-atomic SQL:
| Capability | Edge / HTTP (turbineHttp) | Direct (turbine({ connectionString })) |
|---|---|---|
findMany / findUnique / nested with (json_agg) | ✓ | ✓ |
create / update / delete / upsert (single statement) | ✓ | ✓ |
aggregate / groupBy | ✓ | ✓ |
$transaction / db.transaction() | ✗ → typed error | ✓ |
Streaming cursors (findManyStream) | ✗ → typed error | ✓ |
$listen / $notify (LISTEN/NOTIFY) | ✗ → typed error | ✓ |
Run transactional work, streaming, and realtime on a server or worker over the direct connection; serve reads and single-statement writes from the edge over HTTP.
Why Turbine on BataDB?#
- No WASM at all: the edge entry is held under 61 kB brotli in CI, comfortably inside Cloudflare Workers' 1 MB limit (Prisma 7: ~1.6 MB TS/WASM query compiler)
- Single-query nested reads, one SQL statement via
json_agg, not N+1 - Pipeline batching, N queries in 1 round-trip, which matters most on high-latency serverless
- No adapter layer,
turbineHttp(pool, SCHEMA)with any pg-compatible driver, including@batadata/serverless - SQL-first migrations, no shadow database, no proprietary schema DSL
BataDB also offers scale-to-zero and instant branching. Turbine's pipelining and single-query nested reads cut the number of round trips a request makes, and fewer round trips pairs naturally with wake-on-connect, since a cold compute pays its wake cost once rather than once per query. (Branching is a BataDB feature; Turbine just runs the same typed code against whichever branch your connection string points at.)
Next steps#
- Migrate from Drizzle, your BataDB connection string and
turbine generateare the only BataDB-specific pieces - Migrate from Prisma, same typed API, no WASM engine on the edge
- Serverless & Edge, the full
turbineHttpguide andPgCompatPoolcontract - Transactions & Pipelines, pipelines are the highest-leverage edge optimization
- Typed Errors, the errors you get when you reach for a session-only feature over HTTP