Neon is a serverless Postgres host with database branching, scale-to-zero, and the @neondatabase/serverless driver, which sends queries over HTTP or WebSockets. That driver runs on Node, Bun, Deno, Cloudflare Workers, and AWS Lambda. Neon is also standard Postgres, so plain pg over TCP works too, and on Workers you can reach it through Hyperdrive.
Two drivers, one connection string
@neondatabase/serverlessDATABASE_URLneon(...) or new Pool(...)
sessions & transactionsWebSocket Pool clientmodule scope on Node · per request on Workers
Both clients ship in the same package. Use the HTTP neon() client for one-shot queries anywhere, and the WebSocket Pool when you need sessions or interactive transactions.
1. Provision
Create a project at console.neon.tech and copy the connection string. Set it as DATABASE_URL in your environment. Neon offers a pooled string (host ends in -pooler) for application traffic and a direct one for migrations.
2. Install
bash
pnpm add @neondatabase/serverless
bash
npm install @neondatabase/serverless
bash
yarn add @neondatabase/serverless
bash
bun add @neondatabase/serverless
Use the HTTP neon() client for one-shot queries and non-interactive transactions (sql.transaction([...])). Use Pool or Client when you need a session, interactive transactions, or node-postgres compatibility.
3. HTTP client (any runtime)
src/db/neon.tsts
import { neon } from "@neondatabase/serverless";import type { App } from "@daloyjs/core";export const sql = neon(process.env.DATABASE_URL!);declare module "@daloyjs/core" { interface AppState { sql: typeof sql; }}// Call with the root app, not through app.register(): a decoration made// inside a registered plugin is visible only to that plugin's own routes.export function neonPlugin(app: App) { app.decorate("sql", sql);}
Keep the declare module block in the module that creates the client, not in a separate .d.ts file. Declaration files are exempt from type-checking when skipLibCheck is on (the scaffolded default), so a broken import inside a .d.ts fails silently and state.sql degrades to any.
The tagged template turns every ${value} into a bound parameter. When you build the SQL text yourself, use sql.query(text, params) with $1 placeholders instead of string concatenation:
ts
const rows = await sql.query("select id, email from users where id = $1", [id]);
4. Pooled WebSocket client
Where the pool lives depends on the runtime:
Runtime
Pool guidance
Node, Bun, Deno (long-lived)
One module-scope Pool, closed with app.onClose()
AWS Lambda and other serverless functions
Prefer the HTTP neon() client. If you need a Pool, keep max small, set idleTimeoutMillis, and use the -pooler host; there is no shutdown hook
Cloudflare Workers
Create, use, and close the Pool or Client inside one request, or use Hyperdrive with pg
For a long-lived server:
src/db/neon-pool.tsts
import { Pool } from "@neondatabase/serverless";import type { App } from "@daloyjs/core";const pool = new Pool({ connectionString: process.env.DATABASE_URL! });declare module "@daloyjs/core" { interface AppState { db: Pool; }}// Call with the root app, not through app.register(): a decoration made// inside a registered plugin is visible only to that plugin's own routes.export function neonPoolPlugin(app: App) { app.decorate("db", pool); // Runs on the Node, Bun, and Deno adapters after in-flight requests drain. app.onClose(async () => { await pool.end(); });}
Only the module that creates the pool declares db, so an app that imports just the HTTP client never sees a state.db that is not there.
5. Use it in a route
src/server.tsts
import { z } from "zod";import { App, secureHeaders } from "@daloyjs/core";import { neonPlugin } from "./db/neon.ts";const app = new App();app.use(secureHeaders());neonPlugin(app);const UserSchema = z.object({ id: z.uuid(), email: z.email() });app.get( "/users/:id", { operationId: "getUser", request: { params: z.object({ id: z.uuid() }) }, responses: { 200: { description: "Found", body: UserSchema }, 404: { description: "Not found" }, }, }, async ({ params, state }) => { const rows = await state.sql`select id, email from users where id = ${params.id} limit 1`; const user = rows[0]; return user ? { status: 200, body: user } : { status: 404, body: { type: "about:blank", title: "Not found", status: 404 } }; },);
Cloudflare Workers
Use the HTTP neon() client and read the connection string from the Worker environment instead of process.env. Store it with pnpm wrangler secret put DATABASE_URL, run pnpm wrangler types so env.DATABASE_URL is typed, then import env from cloudflare:workers and decorate the app before registering routes (decorating inside the Worker fetch handler throws on the second request):
src/index.tsts
import { env } from "cloudflare:workers";import { neon } from "@neondatabase/serverless";import { App } from "@daloyjs/core";import { toFetchHandler } from "@daloyjs/core/cloudflare";const app = new App({ production: true, behindProxy: { hops: 1 } });app.decorate("sql", neon(env.DATABASE_URL));// ...register routes that read state.sqlexport default toFetchHandler(app);
The HTTP client is safe to create at module scope because it opens no socket until a query runs inside a request. A WebSocket Pool is not: create it inside the request and close it before the request ends. For pg and ORMs that expect a TCP driver, Neon's driver docs suggest Hyperdrive instead.
With Drizzle ORM
bash
pnpm add drizzle-orm
bash
npm install drizzle-orm
bash
yarn add drizzle-orm
bash
bun add drizzle-orm
src/db/drizzle.tsts
import { drizzle } from "drizzle-orm/neon-http";import { neon } from "@neondatabase/serverless";const sql = neon(process.env.DATABASE_URL!);export const db = drizzle({ client: sql });
With Prisma
Prisma 7 connects through a driver adapter, which is mandatory, and generates the client into your source tree. Use the Neon adapter (current release: Prisma 7.10):
import { PrismaNeon } from "@prisma/adapter-neon";import { PrismaClient } from "../generated/prisma/client.ts";const adapter = new PrismaNeon({ connectionString: process.env.DATABASE_URL! });export const prisma = new PrismaClient({ adapter });
The app uses the pooled string in DATABASE_URL. Prisma CLI commands (prisma migrate, prisma db pull) read their URL from prisma.config.ts, so point that at Neon's direct connection string:
See Prisma for the full plugin, transactions, and shutdown setup.
Branching for preview environments
Pair Neon's branching with Vercel preview deployments or GitHub PR previews. Create a branch per PR and pass its connection string to the deployment's DATABASE_URL. This is a natural fit for the Vercel adapter.