Skip to content

Search docs

Jump between documentation pages.

Browse docs

Use Neon serverless Postgres with DaloyJS

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(...)
edge-friendlyHTTP neon() clientOne-shot queries · Workers · Lambda
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

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:

RuntimePool guidance
Node, Bun, Deno (long-lived)One module-scope Pool, closed with app.onClose()
AWS Lambda and other serverless functionsPrefer 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 WorkersCreate, 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.sql

export 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
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):

bash
pnpm add @prisma/client @prisma/adapter-neon
bash
pnpm add -D prisma
prisma/schema.prismaprisma
datasource db {
  provider = "postgresql"
}

generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}
src/db/prisma.tsts
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:

prisma.config.tsts
import "dotenv/config";
import { defineConfig, env } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  datasource: { url: env("DIRECT_URL") },
});

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.

See also Cloudflare Hyperdrive, PlanetScale, Supabase, and the database hosting overview.

Edit this page on GitHub

Last updated