Serverless driver
The Prisma Postgres serverless driver connects to hosted Prisma Postgres databases over HTTP and WebSockets. Use it with Prisma ORM through @prisma/adapter-ppg, or query with raw SQL through @prisma/ppg.
| Runtime | Recommended connection | Package |
|---|---|---|
| Conventional Node.js or Bun runtime with PostgreSQL TCP support | Pooled TCP | @prisma/adapter-pg with pg |
Edge or TCP-constrained runtime with fetch and WebSocket support |
Prisma Postgres serverless driver | @prisma/adapter-ppg with Prisma ORM, or @prisma/ppg for raw SQL |
Use the serverless driver when a conventional PostgreSQL TCP driver cannot run. It also supports streaming, pipelined queries, transactions, batch operations, SQL template literals, and custom type handling.
If your application currently uses a hosted prisma+postgres:// Accelerate connection, follow Connect to Prisma Postgres without Accelerate.
The serverless driver accepts the direct Prisma Postgres connection-string format:
postgres://identifier:key@db.prisma.io:5432/postgres?sslmode=requireIt uses this value as a credential, then communicates with Prisma Postgres over HTTP and WebSockets. It does not open a TCP connection.
In the Prisma Console, select your database, choose Connect to your database, generate a connection string, and copy the direct value. Do not construct the value by editing another connection string.
If you don't have a Prisma Postgres database, create one using the create-db CLI tool:
bunx create-db@latestpnpm dlx create-db@latestyarn dlx create-db@latestnpx create-db@latest[!WARNING] Keep credentials server-side
Store
DATABASE_URLin server-side runtime secrets. Never commit it to source control or include it in code delivered to a browser.
Install the appropriate package based on your use case:
bun add @prisma/ppg @prisma/adapter-ppgpnpm add @prisma/ppg @prisma/adapter-ppgyarn add @prisma/ppg @prisma/adapter-ppgnpm install @prisma/ppg @prisma/adapter-ppg bun add @prisma/ppg
```Use with Prisma ORM {#use-with-prisma-orm}
Section titled “Use with Prisma ORM {#use-with-prisma-orm}”Use the PrismaPostgresAdapter to connect Prisma Client via the serverless driver.
When you generate Prisma Client for an edge runtime, set the generator's runtime to the deployment target. For example, use workerd for Cloudflare Workers, vercel-edge for Vercel Edge Functions, or deno for Deno:
generator client {
provider = "prisma-client"
output = "../generated/prisma"
runtime = "workerd"
}Generate the client, then instantiate it with the serverless driver adapter:
import { PrismaClient } from "../../generated/prisma/client";
import { PrismaPostgresAdapter } from "@prisma/adapter-ppg";
const prisma = new PrismaClient({
adapter: new PrismaPostgresAdapter({
connectionString: process.env.DATABASE_URL!,
}),
});
const users = await prisma.user.findMany();Use the prismaPostgres() high-level API for SQL template literals with automatic parameterization:
import { prismaPostgres, defaultClientConfig } from "@prisma/ppg";
const ppg = prismaPostgres(defaultClientConfig(process.env.DATABASE_URL!));
type User = { id: number; name: string; email: string };
const users = await ppg.sql<User>`
SELECT * FROM users WHERE email = ${"user@example.com"}
`.collect();
console.log(users[0].name);Results are returned as CollectableIterator<T>. Stream rows one at a time for constant memory usage, or collect all rows into an array:
type User = { id: number; name: string; email: string };
// Stream rows one at a time (constant memory usage)
for await (const user of ppg.sql<User>`SELECT * FROM users`) {
console.log(user.name);
}
// Or collect all rows into an array
const allUsers = await ppg.sql<User>`SELECT * FROM users`.collect();Send multiple queries over a single WebSocket connection without waiting for responses. Queries are sent immediately and results arrive in FIFO order:
import { client, defaultClientConfig } from "@prisma/ppg";
const cl = client(defaultClientConfig(process.env.DATABASE_URL!));
const session = await cl.newSession();
// Send all queries immediately (pipelined)
const [usersResult, ordersResult, productsResult] = await Promise.all([
session.query("SELECT * FROM users"),
session.query("SELECT * FROM orders"),
session.query("SELECT * FROM products"),
]);
session.close();Pipelining reduces network round trips by sending multiple queries before waiting for their responses. The effect on end-to-end latency depends on network conditions and query execution time.
Parameters over 1KB are automatically streamed without buffering in memory. For large binary parameters, you must use boundedByteStreamParameter() which creates a BoundedByteStreamParameter object that carries the total byte size, required by the PostgreSQL protocol:
import { client, defaultClientConfig, boundedByteStreamParameter, BINARY } from "@prisma/ppg";
const cl = client(defaultClientConfig(process.env.DATABASE_URL!));
// Large binary data (e.g., file content)
const stream = getReadableStream(); // Your ReadableStream source
const totalSize = 1024 * 1024; // Total size must be known in advance
// Create a bounded byte stream parameter
const streamParam = boundedByteStreamParameter(stream, BINARY, totalSize);
// Automatically streamed - constant memory usage
await cl.query("INSERT INTO files (data) VALUES ($1)", streamParam);For Uint8Array data, use byteArrayParameter():
import { client, defaultClientConfig, byteArrayParameter, BINARY } from "@prisma/ppg";
const cl = client(defaultClientConfig(process.env.DATABASE_URL!));
const bytes = new Uint8Array([1, 2, 3, 4]);
const param = byteArrayParameter(bytes, BINARY);
await cl.query("INSERT INTO files (data) VALUES ($1)", param);The boundedByteStreamParameter() function is provided by the @prisma/ppg library and requires the total byte size to be known in advance due to PostgreSQL protocol requirements.
Transactions automatically handle BEGIN, COMMIT, and ROLLBACK:
const result = await ppg.transaction(async (tx) => {
await tx.sql.exec`INSERT INTO users (name) VALUES ('Alice')`;
const users = await tx.sql<User>`SELECT * FROM users WHERE name = 'Alice'`.collect();
return users[0].name;
});Batch operations execute multiple statements in a single round-trip within an automatic transaction:
const [users, affected] = await ppg.batch<[User[], number]>(
{ query: "SELECT * FROM users WHERE id < $1", parameters: [5] },
{ exec: "INSERT INTO users (name) VALUES ($1)", parameters: ["Charlie"] },
);When using defaultClientConfig(), common PostgreSQL types are automatically parsed (boolean, int2, int4, int8, float4, float8, text, varchar, json, jsonb, date, timestamp, timestamptz):
import { prismaPostgres, defaultClientConfig } from "@prisma/ppg";
const ppg = prismaPostgres(defaultClientConfig(process.env.DATABASE_URL!));
// JSON/JSONB automatically parsed
const rows = await ppg.sql<{ data: { key: string } }>`
SELECT '{"key": "value"}'::jsonb as data
`.collect();
console.log(rows[0].data.key); // "value"
// BigInt parsed to JavaScript BigInt
const bigints = await ppg.sql<{
big: bigint;
}>`SELECT 9007199254740991::int8 as big`.collect();
// Dates parsed to Date objects
const dates = await ppg.sql<{
created: Date;
}>`SELECT NOW() as created`.collect();Extend or override the type system with custom parsers (by PostgreSQL OID) and serializers (by type guard):
import { client, defaultClientConfig } from "@prisma/ppg";
import type { ValueParser } from "@prisma/ppg";
// Custom parser for UUID type
const uuidParser: ValueParser<string | null> = {
oid: 2950,
parse: (value) => (value ? value.toUpperCase() : null),
};
const config = defaultClientConfig(process.env.DATABASE_URL!);
const cl = client({
...config,
parsers: [...(config.parsers ?? []), uuidParser], // Append to defaults
});For custom serializers, place them before defaults so they take precedence:
import { client, defaultClientConfig } from "@prisma/ppg";
import type { ValueSerializer } from "@prisma/ppg";
class Point {
constructor(
public x: number,
public y: number,
) {}
}
const pointSerializer: ValueSerializer<Point> = {
supports: (value: unknown): value is Point => value instanceof Point,
serialize: (value: Point) => `(${value.x},${value.y})`,
};
const config = defaultClientConfig(process.env.DATABASE_URL!);
const cl = client({
...config,
serializers: [pointSerializer, ...(config.serializers ?? [])], // Your serializer first
});
await cl.query("INSERT INTO locations (point) VALUES ($1)", new Point(10, 20));See the npm package documentation for more details.
The driver works in server-side environments with fetch and WebSocket APIs:
| Platform | HTTP Transport | WebSocket Transport |
|---|---|---|
| Cloudflare Workers | ✅ | ✅ |
| Vercel Edge Functions | ✅ | ✅ |
| AWS Lambda | ✅ | ✅ |
| Deno Deploy | ✅ | ✅ |
| Bun | ✅ | ✅ |
| Node.js 18+ | ✅ | ✅ |
The package can run in browser environments, but a database connection string is a server credential. Do not use the driver in browser-delivered code. Route browser requests through a server-side endpoint instead.
- HTTP transport (stateless): Each query is an independent HTTP request. Best for simple queries and edge functions.
- WebSocket transport (stateful): Persistent connection for multiplexed queries. Best for transactions, pipelining, and multiple queries. Create a session with
client().newSession().
High-level API with SQL template literals, transactions, and batch operations. Recommended for most use cases.
Low-level API with explicit parameter passing and session management. Use when you need fine-grained control.
See the npm package for complete API documentation.
Structured error types are provided: DatabaseError, HttpResponseError, WebSocketError, ValidationError.
import { DatabaseError } from "@prisma/ppg";
try {
await ppg.sql`SELECT * FROM invalid_table`.collect();
} catch (error) {
if (error instanceof DatabaseError) {
console.log(error.code);
}
}The serverless driver uses Prisma Postgres connection pooling by default and requires no additional pool configuration. See the Prisma Postgres regions.
- Requires a Prisma Postgres instance and does not work with Local Postgres databases