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 engine — ~22 kB brotli on the edge, fits Cloudflare Workers' 1 MB limit (Prisma: 1.6 MB WASM)
- 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