Serverless & Edge
turbine-orm/serverless binds Turbine to a pool you supply, so the full query API runs on edge and serverless runtimes. The edge entry is under 117 kB brotli and has no WASM.
The pattern#
import { turbineHttp } from 'turbine-orm/serverless';
import type { TurbineClient } from './generated/turbine/index.js';
import { SCHEMA } from './generated/turbine/metadata.js';
import { Pool } from '<your-edge-driver>';
const pool = new Pool({ connectionString: process.env.DATABASE_URL! });
export const db = turbineHttp<TurbineClient>(pool, SCHEMA);SCHEMA is the metadata object npx turbine generate writes. It has no side effects and no Node-only imports, so it loads in any runtime.
The <TurbineClient> type argument gives you the generated accessors (db.users.findMany()). Without it you get the base client and reach tables with db.table('users').
The optional third argument takes every client option except pool: globalFilters, errorMessages, logger, diagnostics, defaultLimit and the rest.
What changes with a supplied pool#
db.disconnect()does nothing. You own the pool's lifecycle.- Turbine registers no global type parsers, so your own queries on the pool keep the driver's parsing. Turbine's queries still read
bigintas a number and zone-lesstimestamp/dateas UTC (utcTimestamps: falseturns that off). - Statements are sent unnamed by default, so a transaction-mode pooler works with no extra setting.
- A pool with no
connect(), such as a stateless HTTP driver, runs one statement at a time.$transaction,transaction(), a batchedpipeline, cursor streams and any write that takes several statements (a nested write, a chunkedcreateMany) throwUnsupportedFeatureError(TURBINE_E017) there.
Set diagnostics: 'production' on a runtime that has process but no NODE_ENV, such as Cloudflare Workers with Node compatibility. Without it the client runs in development mode, with per-query advice and the SQL-cache self-check on. See Diagnostics.
export const db = turbineHttp<TurbineClient>(pool, SCHEMA, { diagnostics: 'production' });Neon#
npm install @neondatabase/serverless// db.ts
import { Pool } from '@neondatabase/serverless';
import { turbineHttp } from 'turbine-orm/serverless';
import type { TurbineClient } from './generated/turbine/index.js';
import { SCHEMA } from './generated/turbine/metadata.js';
const pool = new Pool({ connectionString: process.env.DATABASE_URL! });
export const db = turbineHttp<TurbineClient>(pool, SCHEMA);// app/api/users/route.ts (Next.js Edge)
export const runtime = 'edge';
import { db } from '@/db';
export async function GET() {
const users = await db.users.findMany({ limit: 20 });
return Response.json(users);
}For the shortest setup, see Turbine + Neon.
Vercel Postgres#
npm install @vercel/postgresimport { createPool } from '@vercel/postgres';
import { turbineHttp } from 'turbine-orm/serverless';
import type { TurbineClient } from './generated/turbine/index.js';
import { SCHEMA } from './generated/turbine/metadata.js';
const pool = createPool();
export const db = turbineHttp<TurbineClient>(pool, SCHEMA);Vercel Postgres wraps Neon. If you use Neon directly, @neondatabase/serverless saves a package.
Cloudflare Hyperdrive#
Hyperdrive pools connections in front of any PostgreSQL database and exposes them to Workers over TCP. Use pg with the Node compatibility flag.
# wrangler.toml
compatibility_flags = ["nodejs_compat"]
[[hyperdrive]]
binding = "HYPERDRIVE"
id = "<your-hyperdrive-id>"import { Pool } from 'pg';
import { turbineHttp } from 'turbine-orm/serverless';
import type { TurbineClient } from './generated/turbine/index.js';
import { SCHEMA } from './generated/turbine/metadata.js';
export default {
async fetch(req: Request, env: Env) {
const pool = new Pool({ connectionString: env.HYPERDRIVE.connectionString });
const db = turbineHttp<TurbineClient>(pool, SCHEMA, { diagnostics: 'production' });
const users = await db.users.findMany({ limit: 20 });
return Response.json(users);
},
};Use Hyperdrive for Workers backed by a regular PostgreSQL instance, such as AWS RDS, Supabase or a self-hosted server.
Supabase#
import { Pool } from 'pg';
import { turbineHttp } from 'turbine-orm/serverless';
import type { TurbineClient } from './generated/turbine/index.js';
import { SCHEMA } from './generated/turbine/metadata.js';
const pool = new Pool({
connectionString: process.env.DATABASE_URL!, // the pooler URL
ssl: { rejectUnauthorized: true },
});
export const db = turbineHttp<TurbineClient>(pool, SCHEMA);For serverless functions, use Supabase's pooler URL (...pooler.supabase.com:6543), not the direct ...db.supabase.co:5432 URL. The pooler runs in transaction mode, which keeps short-lived functions from exhausting connections.
Other drivers#
Any driver works once it matches PgCompatPool, exported from turbine-orm with PgCompatPoolClient and PgCompatQueryResult. The contract is three methods: query(text, values), connect() and end(). A driver such as postgres.js needs a small adapter to that shape.
Keep edge queries bounded#
Edge runtimes have small memory limits, and a nested findMany builds its whole result in memory.
- Put a
limiton every root query, and paginate. - Put a
limiton every to-many relation inwith:posts: { limit: 20 }, notposts: true.
const users = await db.users.findMany({
limit: 20,
with: { posts: { limit: 5 } },
});Cold start#
Importing the edge entry opens no connection. Under the edge-light and browser conditions (Vercel Edge, Next.js edge routes) it does not bundle pg at all. Cold start is your driver's cold start.
Streaming on the edge#
findManyStream needs a pool that can check out a connection and run DECLARE CURSOR, such as Neon's Pool, Hyperdrive or Supabase. It suits writing a large result to object storage more than answering a request.
A stream left open when an invocation freezes does not hold its connection forever. The server ends its transaction after streamIdleTimeoutMs (five minutes by default) plus 10 seconds, and the next pull throws ConnectionError (TURBINE_E004). See Abandoned streams.
See also#
- Turbine + Neon and Turbine + BataDB: quick starts.
- Transactions & Pipelines: pipelines save the most round trips on the edge.
- Relations: payload size matters most on the edge.
- Reading and writing: everything works the same under
turbineHttp.