03/20/26

Neon Serverless Postgres: Complete Guide for TypeScript Developers

How to use Neon's serverless PostgreSQL with modern TypeScript backends

12 Min Read

Most TypeScript backends need a database, and most of the time that database is PostgreSQL. The question is how you run it. Traditional managed Postgres (RDS, Cloud SQL) means picking an instance size upfront, paying for it whether you use it or not, and scaling manually when traffic grows. Neon takes a different approach: serverless Postgres that scales to zero when idle and branches like Git.

This guide covers what Neon is, how to use it with common TypeScript tools (Drizzle, Prisma), and how Encore.ts makes it even simpler with automatic database provisioning and zero connection string management.

Already familiar with Neon and just want to set it up? Skip to Using Neon with Encore.ts for the fastest path from zero to production Postgres.

What Is Neon?

Neon is a serverless PostgreSQL provider. It runs standard Postgres (the latest stable Postgres version) with a storage layer rewritten from scratch to separate compute from storage. This architecture enables three things that traditional managed Postgres can't do:

  1. Scale to zero. Compute shuts down after a period of inactivity. You don't pay for idle databases. When a connection comes in, compute starts up again (cold starts are typically 300-500ms).

  2. Database branching. Create a copy-on-write branch of your database in seconds, regardless of size. Branches share the underlying storage pages, so a branch of a 100GB database costs almost nothing until you start writing new data to it.

  3. Instant point-in-time restore. The storage engine keeps a history of changes. You can restore to any point in the last 6 hours on the free tier, 7 days on Launch, and 30 days on Scale and above without maintaining manual backups.

Under the hood, it's still Postgres. Your existing queries, extensions, ORMs, and migration tools all work. Neon adds a connection pooler (built on PgBouncer), a web console, and APIs for managing branches and compute endpoints.

Neon's Free Tier

Neon's free tier is generous enough for development and small production workloads:

  • 0.5 GB storage
  • 1 project with 10 branches
  • 100 compute hours per month
  • Shared compute (0.25 vCPU)

For hobby projects and early-stage apps, this means zero database costs until you outgrow it.

Why Neon for TypeScript Backends

TypeScript backend developers typically care about three things when choosing a database provider: how fast they can get started, how the database works in their development workflow, and how it scales when real traffic shows up.

Development Speed

Neon databases are created in seconds through the web console or API. There's no instance provisioning, no VPC configuration, no waiting for a cluster to boot. You get a connection string immediately and start writing queries.

For TypeScript specifically, Neon provides a @neondatabase/serverless driver that works over HTTP and WebSocket. This matters for serverless runtimes (Cloudflare Workers, Vercel Edge Functions) where traditional TCP-based Postgres drivers don't work. If you're running a standard Node.js server, the regular pg driver works fine too.

Branching for Preview Environments

Database branching is where Neon stands out for development workflows. When you open a pull request, you can create a database branch that contains a copy of your production data (or staging data). The branch is isolated, so you can run migrations against it without affecting anyone else. When you merge or close the PR, you delete the branch.

This solves a common problem in backend development: either everyone shares a staging database (and steps on each other), or you maintain separate databases for each developer (which drift out of sync). Branches give you the isolation of separate databases with the data fidelity of a shared one.

Connection Pooling

Neon includes built-in connection pooling through PgBouncer. Each connection string comes in two variants: a direct connection (for migrations and admin tasks) and a pooled connection (for application queries). The pooled endpoint handles hundreds of concurrent connections without hitting Postgres connection limits.

This matters for TypeScript backends because Node.js applications often open many connections, especially when running multiple service instances or handling concurrent requests. With traditional Postgres hosting, you'd add PgBouncer yourself. Neon includes it by default.

Getting Started with Neon

Creating a Database

  1. Sign up at neon.com
  2. Create a project (this creates your first database)
  3. Choose your region (pick the one closest to your application servers)

Neon gives you a connection string immediately:

postgresql://username:password@ep-example-123.us-east-2.aws.neon.com/neondb?sslmode=require

You also get a pooled connection string for application use:

postgresql://username:password@ep-example-123-pooler.us-east-2.aws.neon.com/neondb?sslmode=require

Setting Up Locally

Store your connection string in an environment variable:

export DATABASE_URL="postgresql://username:[email protected]/neondb?sslmode=require"

For local development, you have two options: connect directly to a Neon development branch, or run Postgres locally and only use Neon in staging and production. A local database is faster for iteration (no network round-trip), but a Neon branch gives you production-like data.

Using Neon with Common TypeScript Tools

Neon is standard Postgres, so any TypeScript library that connects to Postgres works. Here are the most common setups.

With Drizzle ORM

Drizzle is a TypeScript ORM that generates type-safe queries with a SQL-like API. It works well with Neon because it supports both the standard pg driver and Neon's serverless driver.

Install dependencies:

npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit

Define your schema:

// schema.ts
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  createdAt: timestamp("created_at").defaultNow(),
});

Connect using Neon's serverless driver:

// db.ts
import { drizzle } from "drizzle-orm/neon-serverless";
import { Pool } from "@neondatabase/serverless";
import * as schema from "./schema";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

export const db = drizzle(pool, { schema });

Query with full type safety:

import { db } from "./db";
import { users } from "./schema";
import { eq } from "drizzle-orm";

// Insert
await db.insert(users).values({
  name: "Alice",
  email: "[email protected]",
});

// Select with type-safe where clause
const user = await db.query.users.findFirst({
  where: eq(users.email, "[email protected]"),
});

With Prisma

Prisma works with Neon through its standard PostgreSQL adapter. No special configuration is needed beyond the connection string.

// schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        Int      @id @default(autoincrement())
  name      String
  email     String   @unique
  createdAt DateTime @default(now())
}

For serverless environments, Prisma also offers a Neon adapter:

import { Pool } from "@neondatabase/serverless";
import { PrismaNeon } from "@prisma/adapter-neon";
import { PrismaClient } from "@prisma/client";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

const adapter = new PrismaNeon(pool);
const prisma = new PrismaClient({ adapter });

With the pg Driver (Raw SQL)

If you prefer writing SQL directly, the standard pg library works with no changes:

import { Pool } from "pg";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

const result = await pool.query(
  "SELECT * FROM users WHERE email = $1",
  ["[email protected]"]
);

For serverless runtimes, swap in Neon's driver:

import { Pool } from "@neondatabase/serverless";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

// Same query API as pg
const result = await pool.query(
  "SELECT * FROM users WHERE email = $1",
  ["[email protected]"]
);

Using Neon with Encore.ts

The approaches above all work, but they share a common overhead: you're managing connection strings, configuring pooling, writing migration scripts, and setting up different databases for each environment. This is the kind of infrastructure plumbing that adds up across services.

Encore.ts takes a different approach. Databases are declared as part of your application code. Encore handles provisioning, migrations, connection pooling, and per-environment configuration automatically.

Encore has a built-in integration with Neon. When you deploy through Encore Cloud, your databases are automatically provisioned on Neon with the right configuration for each environment. No connection strings to copy, no pooler setup, no manual branch creation.

Declaring a Database

In Encore, you declare a database by creating a SQLDatabase instance and putting your migration files in a migrations directory:

// users/db.ts
import { SQLDatabase } from "encore.dev/storage/sqldb";

const db = new SQLDatabase("users", {
  migrations: "./migrations",
});

export default db;

Create your migration:

-- users/migrations/1_create_users.up.sql
CREATE TABLE users (
  id BIGSERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  email TEXT NOT NULL UNIQUE,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

That's it. When you run encore run, Encore creates a local Postgres database, runs the migration, and gives you a type-safe query API. When you deploy, the same declaration provisions a Neon database in the cloud. This structured approach also means AI coding agents can generate database queries that match your actual schema - when an agent sees new SQLDatabase("users", { migrations: "./migrations" }), it knows the database exists and can write queries against the migration-defined schema. Encore's MCP server takes this further by giving agents access to live database schemas, so generated queries are always valid.

Querying

Encore provides a built-in query API. No ORM installation, no connection management:

import db from "./db";

// Type-safe row mapping
interface User {
  id: number;
  name: string;
  email: string;
}

// Query
const user = await db.queryRow<User>`
  SELECT id, name, email FROM users WHERE email = ${email}
`;

// Insert
await db.exec`
  INSERT INTO users (name, email)
  VALUES (${name}, ${email})
`;

You can also use Drizzle or Prisma on top of Encore's database if you prefer an ORM. Encore handles the connection; the ORM handles the query building.

Automatic Neon Provisioning

When you connect your Encore app to Encore Cloud, databases are provisioned on Neon automatically:

  • Local development: Encore runs a local Postgres instance. No external dependencies needed.
  • Preview environments: Each pull request gets its own Neon branch with isolated data. Branches are created and destroyed automatically.
  • Staging and production: Dedicated Neon projects with the compute size and region you configure.

The connection string, pooling configuration, and TLS settings are all handled by the platform. Your application code stays the same across environments. Read more about Encore's database support in the database docs and the Neon integration docs.

Why This Approach Works Better

The manual approach (Neon + Drizzle/Prisma + environment variables) works. Plenty of teams ship this way. But it means every developer on the team needs to set up their own Neon branch (or local Postgres), keep their environment variables in sync, and remember to run migrations after pulling new code.

With Encore, encore run handles all of that. New developer joins the team, clones the repo, runs encore run, and has a working local database with the latest schema. Open a pull request, and the preview environment has its own database branch. Merge to main, and the migration runs automatically in production. The database is part of the code, not a separate thing you configure.

Neon Features for Production

Once you're running in production, Neon has several features that make operations easier.

Autoscaling

Neon compute scales based on load. You configure a minimum and maximum compute size (measured in Compute Units, where 1 CU is roughly 1 vCPU and 4GB RAM). The database scales up during traffic spikes and back down during quiet periods.

On the free and Launch plans, scaling isn't instant. There can be a brief latency increase during scale-up. On the Scale and Business plans, Neon keeps a warm pool of compute ready, making scale transitions smoother.

Branching Workflows

Beyond preview environments, branching is useful for:

  • Testing migrations: Branch production, run the migration on the branch, verify it works, then run it on production.
  • Analytics queries: Branch production and run expensive analytical queries on the branch without affecting production performance.
  • Data seeding: Create a branch, modify data for a specific test scenario, use it for QA, then delete it.

Branches can be created through the Neon console, CLI, or API:

# Create a branch from the main branch
neonctl branches create --name feature-xyz --project-id your-project-id

# Get the connection string for the new branch
neonctl connection-string feature-xyz --project-id your-project-id

Monitoring and Observability

Neon's dashboard shows query performance, connection counts, compute utilization, and storage usage. For deeper monitoring, you can connect Neon to external tools:

  • pg_stat_statements is enabled by default for query performance analysis
  • Connection pooler metrics show active, idle, and waiting connections
  • Storage metrics track data size and history retention

Read Replicas

Neon supports read replicas that run on separate compute endpoints. Unlike traditional Postgres replicas, Neon replicas share the same storage layer, so there's no replication lag for committed writes. This is useful for separating read-heavy analytics workloads from your primary application traffic.

# Create a read replica
neonctl endpoints create --type read_replica --branch main --project-id your-project-id

Cost Considerations

Neon's pricing is based on compute hours and storage:

PlanComputeStorageBranchingPrice
Free100 CU-hours/mo (shared)0.5 GB10 branches$0
LaunchUsage-based10 GB10 branches~$19/mo typical
ScaleUsage-based50 GB25 branches~$69/mo typical
BusinessUsage-based500 GBUnlimitedContact sales

For comparison, a small RDS instance (db.t4g.micro) costs around $12/month but runs 24/7 whether you use it or not. Neon's paid plans are usage-based, so actual costs depend on how many compute hours you consume. For workloads with variable traffic or long idle periods, Neon's scale-to-zero model can be significantly cheaper. For consistently high-traffic workloads, fixed-compute options may be more predictable.

Common Patterns

Multi-Tenant Databases

For SaaS applications, you have two options with Neon:

  1. Schema-per-tenant: One database, separate schemas. Works well up to a few hundred tenants. Use Postgres SET search_path to route queries.

  2. Database-per-tenant: Separate Neon projects or branches per tenant. Better isolation, but more management overhead.

For most TypeScript backends, the schema-per-tenant approach is simpler and Postgres handles it well.

Connection Management in Serverless

If your TypeScript backend runs on a serverless platform (Lambda, Cloudflare Workers, Vercel), use Neon's serverless driver over HTTP instead of maintaining persistent connections:

import { neon } from "@neondatabase/serverless";

const sql = neon(process.env.DATABASE_URL!);

// Each invocation gets a fresh connection over HTTP
const users = await sql`SELECT * FROM users WHERE active = true`;

This avoids the cold-start connection overhead and connection limit issues that serverless functions face with traditional Postgres drivers.

Migrations

For migration management, the choice depends on your setup:

  • Drizzle Kit: npx drizzle-kit push or npx drizzle-kit migrate for Drizzle users
  • Prisma Migrate: npx prisma migrate deploy for Prisma users
  • Plain SQL files: Tools like golang-migrate or dbmate for teams that prefer writing SQL directly
  • Encore: Migrations run automatically. Put .up.sql files in the migrations directory, and Encore applies them on deploy.

Conclusion

Neon makes PostgreSQL work the way developers expect databases to work in 2026: instant provisioning, no capacity planning, and branching workflows that fit how teams actually build software.

For TypeScript backends, the fastest path from "I need a database" to "it's running in production" is combining Neon with a framework that handles the infrastructure layer. Encore.ts has built-in Neon integration that automates provisioning, migrations, connection pooling, and per-environment configuration. You declare a database in code, and it exists everywhere your app runs.

Get started with Encore and Neon:

curl -L https://encore.dev/install.sh | bash
encore app create my-app --example=ts/hello-world

Read the database documentation for the full API reference, and the Neon integration guide for cloud deployment configuration.

Ready to build your next backend?

Encore is the Open Source framework for building robust type-safe distributed systems with declarative infrastructure.