A complete 2026 guide to serverless Postgres for your Next.js app — from first connection to production-ready patterns with Prisma, caching, and deployment.
G-Tech Blog | 2026 | 18 min readNeon is a serverless PostgreSQL platform and Next.js is the most popular React framework for full-stack development. Together they form one of the most powerful and developer-friendly stacks available in 2026 — scalable, fast, and free to start. This guide walks you through everything from creating your first Neon project to building production-ready query patterns, integrating Prisma, handling migrations, deploying to Vercel, and keeping your database secure.
Before diving into the setup, it is worth understanding why this combination has become so popular. Traditional PostgreSQL databases require you to manage a persistent server — provisioning, scaling, paying for compute even when idle, and handling connection limits carefully. Neon removes all of that. It's serverless Postgres: the database scales to zero when not in use, wakes up automatically when a query arrives, and charges you only for the compute you actually consume.
This aligns perfectly with how Next.js works. When you deploy a Next.js app to Vercel, your Server Components, API Routes, and Server Actions all run as serverless functions — short-lived processes that spin up per request. A traditional database connection pool does not work well in this environment because serverless functions can't maintain persistent connections between invocations. Neon's serverless driver solves this by using HTTP-based queries that are stateless and edge-compatible, making it the ideal database layer for a serverless Next.js deployment.
Before following this guide, make sure you have the following in place. If you are missing any of these, the links below will help you get set up quickly.
The first step is creating your Neon project, which provisions a serverless PostgreSQL database in seconds. Unlike traditional database setup, there is no server to configure, no storage volume to allocate, and no firewall rules to set up manually. Neon handles all of that for you.
my-nextjs-app.
main, a default database
named neondb, and a default role.You'll see several connection options in the dashboard: a direct connection string, a pooled connection string, and environment variable snippets ready to paste. We'll use all of these at different stages of this guide.
The connection string will look like this:
postgresql://USER:PASSWORD@YOUR-HOST.neon.tech/DBNAME?sslmode=require
The pooled connection string (for production) looks slightly different — it routes through Neon's PgBouncer connection pooler and is prefixed differently:
postgresql://USER:PASSWORD@YOUR-HOST-pooler.neon.tech/DBNAME?sslmode=require&pgbouncer=true
main by default. Think of branches like Git branches —
you can create separate branches for development, staging, and preview environments, each with their own
isolated data. We'll cover this in the Neon Branching section later.
If you do not already have a Next.js project, create one using the official CLI. The App Router (introduced in Next.js 13 and now the default) is recommended for new projects because it works smoothly with React Server Components, which are the best place to run database queries in a Next.js application.
npx create-next-app@latest my-neon-app
cd my-neon-app
During setup, select the following options when prompted:
@/*)If you already have an existing Next.js project, simply open it in your editor and continue from Step 3. The Neon driver works with both the App Router and Pages Router, though the code examples in this guide use the App Router.
Neon provides an official serverless driver that is purpose-built for edge and serverless environments. Unlike
the standard pg Node.js driver, which uses persistent TCP connections and does not work well in
serverless functions, the Neon driver uses HTTP and WebSockets to communicate with the database. This makes it
stateless, fast to initialize, and compatible with Vercel's Edge Runtime if needed.
npm install @neondatabase/serverless
Or with pnpm:
pnpm add @neondatabase/serverless
The @neondatabase/serverless package exports a neon function that accepts your
connection string and returns a tagged template literal query interface. This interface lets you write SQL
queries with interpolated variables in a way that is safe from SQL injection by default — the driver
automatically parameterizes your values.
pg package separately. The Neon serverless driver handles
the connection internally. If you are using Prisma (covered in Step 8), the driver is used as the underlying
transport for Prisma's connection adapter.
Environment variables are the secure way to store your database connection string without hardcoding it into
your source code. Next.js reads a .env.local file automatically during development, and environment
variables set in your deployment platform (like Vercel) are used in production.
.env.localIn the root directory of your project, create a file named .env.local and add your connection
strings:
# Direct connection — use for migrations and Prisma introspection
DATABASE_URL="postgresql://USER:PASSWORD@YOUR-HOST.neon.tech/DBNAME?sslmode=require"
# Pooled connection — use for application queries in production
DATABASE_URL_UNPOOLED="postgresql://USER:PASSWORD@YOUR-HOST-pooler.neon.tech/DBNAME?sslmode=require&pgbouncer=true"
.env.local to version control. Add it to your
.gitignore file immediately. Your database password gives full access to your data — treat it
like a private key. If you accidentally commit it, rotate the password in the Neon dashboard immediately and
revoke the old credentials.
In Next.js, environment variables are only available on the server by default. To use them in Server
Components, Server Actions, and API Routes, you reference them with process.env.DATABASE_URL. If
you ever need an environment variable on the client side (browser), you must prefix it with
NEXT_PUBLIC_ — but never expose your database credentials on the client.
.gitignoreConfirm that .env.local is listed. The default Next.js .gitignore already includes
it, but double-check:
# .gitignore
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
React Server Components (RSC) are the ideal place to run database queries in the Next.js App Router. They run
exclusively on the server, never in the browser, which means your database credentials and query logic are never
exposed to the client. They also support async/await at the top level, making it clean to fetch
data and render it in the same component without any client-side state or loading effects.
Replace the contents of app/page.tsx with the following:
import { neon } from '@neondatabase/serverless';
async function getVersion(): Promise<string> {
const sql = neon(process.env.DATABASE_URL!);
const rows = await sql`SELECT version()`;
return rows[0].version as string;
}
export default async function HomePage() {
const version = await getVersion();
return (
<main style={{ padding: '2rem' }}>
<h1>Connected to Neon! “&</h1>
<p>PostgreSQL version: {version}</p>
</main>
);
}
Notice how clean this is. There's no useEffect, no loading state, no client-side fetch. The
component is an async function that awaits the database query directly. The rendered HTML is sent
to the browser already populated with data — which also means better SEO and faster perceived load times.
Let us extend this to query actual application data. First, create a table in Neon using the SQL editor in the Neon dashboard or by running a query:
import { neon } from '@neondatabase/serverless';
type Post = {
id: number;
title: string;
body: string;
created_at: string;
};
async function getPosts(): Promise<Post[]> {
const sql = neon(process.env.DATABASE_URL!);
const rows = await sql`
SELECT id, title, body, created_at
FROM posts
ORDER BY created_at DESC
LIMIT 10
`;
return rows as Post[];
}
export default async function PostsPage() {
const posts = await getPosts();
return (
<main style={{ padding: '2rem' }}>
<h1>Latest Posts</h1>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
<small>{new Date(post.created_at).toLocaleDateString()}</small>
</article>
))}
</main>
);
}
Server Actions are Next.js's mechanism for running server-side code in response to user interactions — form
submissions, button clicks, and mutations. They are defined with the 'use server' directive and can
be called directly from a Client or Server Component without building a separate API endpoint. Combined with
Neon, they make it trivially easy to build forms that write to your database.
import { neon } from '@neondatabase/serverless';
import { revalidatePath } from 'next/cache';
async function createPost(formData: FormData) {
'use server';
const title = formData.get('title') as string;
const body = formData.get('body') as string;
// Basic validation
if (!title?.trim() || !body?.trim()) {
throw new Error('Title and body are required');
}
const sql = neon(process.env.DATABASE_URL!);
// Parameterised — safe from SQL injection
await sql`
INSERT INTO posts (title, body, created_at)
VALUES (${title}, ${body}, NOW())
`;
// Revalidate the posts page so it shows the new post
revalidatePath('/posts');
}
export default function NewPostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Post title" required />
<textarea name="body" placeholder="Write your post..." required />
<button type="submit">Publish Post</button>
</form>
);
}
Notice the call to revalidatePath('/posts') after the insert. This tells Next.js to invalidate the
cached version of the /posts route so that the next visitor sees the freshly updated data. Without
this, Next.js would continue serving the cached page even after the new post was inserted.
Sometimes you need a traditional REST API endpoint — for use by a mobile client, a third-party service, or a part of your frontend that uses client-side fetching. Next.js App Router API Routes (called Route Handlers) let you define these alongside your pages. They run on the server and have full access to your environment variables and Neon connection.
Create the file app/api/posts/route.ts:
import { neon } from '@neondatabase/serverless';
import { NextResponse } from 'next/server';
export async function GET() {
try {
const sql = neon(process.env.DATABASE_URL!);
const posts = await sql`
SELECT id, title, body, created_at
FROM posts
ORDER BY created_at DESC
`;
return NextResponse.json({ posts }, { status: 200 });
} catch (error) {
console.error('Database error:', error);
return NextResponse.json(
{ error: 'Failed to fetch posts' },
{ status: 500 }
);
}
}
import { neon } from '@neondatabase/serverless';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { title, content } = body;
if (!title || !content) {
return NextResponse.json(
{ error: 'Title and content are required' },
{ status: 400 }
);
}
const sql = neon(process.env.DATABASE_URL!);
const result = await sql`
INSERT INTO posts (title, body, created_at)
VALUES (${title}, ${content}, NOW())
RETURNING id, title, created_at
`;
return NextResponse.json({ post: result[0] }, { status: 201 });
} catch (error) {
console.error('Database error:', error);
return NextResponse.json(
{ error: 'Failed to create post' },
{ status: 500 }
);
}
}
While the raw Neon driver is great for simple queries, most production applications benefit from using an ORM. Prisma is the most popular ORM in the Next.js ecosystem and integrates with Neon using a dedicated connection adapter. Prisma gives you type-safe queries based on your database schema, automatic TypeScript type generation, and a powerful migration system.
npm install prisma @prisma/client @prisma/adapter-neon @neondatabase/serverless ws
npm install -D @types/ws
npx prisma init
This creates a prisma/schema.prisma file and adds DATABASE_URL to your
.env file. Update your schema.prisma to use the Neon adapter:
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DATABASE_URL_UNPOOLED")
}
Add your models to prisma/schema.prisma:
model Post {
id Int @id @default(autoincrement())
title String
body String
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
Create the file lib/prisma.ts. In development, Next.js hot-reloads modules frequently, which
would create too many Prisma client instances. The singleton pattern prevents this:
import { PrismaClient } from '@prisma/client';
import { PrismaNeon } from '@prisma/adapter-neon';
import { neonConfig, Pool } from '@neondatabase/serverless';
import ws from 'ws';
// Required for Node.js environments (not needed on Vercel Edge)
neonConfig.webSocketConstructor = ws;
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
function createPrismaClient() {
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = new PrismaNeon(pool);
return new PrismaClient({ adapter });
}
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
import { prisma } from '@/lib/prisma';
export default async function PostsPage() {
const posts = await prisma.post.findMany({
where: { published: true },
include: { author: true },
orderBy: { createdAt: 'desc' },
take: 10,
});
return (
<main>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>By {post.author?.name ?? 'Anonymous'}</p>
</article>
))}
</main>
);
}
Prisma Migrate handles schema changes through versioned migration files. When you modify your
schema.prisma, you generate a migration that contains the SQL to update your database, and Prisma
applies it in order. This gives you a full history of schema changes and makes it safe to evolve your database
alongside your application code.
# Generate migration SQL and apply it to your Neon database
npx prisma migrate dev --name init
# Generate the Prisma client based on your schema
npx prisma generate
The migrate dev command connects using your DATABASE_URL direct connection (not the
pooler), creates a prisma/migrations directory with the SQL file, and applies the migration to
your Neon database.
Create prisma/seed.ts:
import { prisma } from '../lib/prisma';
async function main() {
await prisma.user.create({
data: {
email: 'alice@example.com',
name: 'Alice',
posts: {
create: [
{ title: 'Hello Neon', body: 'My first post!', published: true },
{ title: 'Draft post', body: 'Coming soon...', published: false },
],
},
},
});
console.log('Seed complete');
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
Add to package.json:
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
}
Then run:
npx prisma db seed
PostgreSQL has a hard limit on the number of simultaneous connections. In a serverless environment where dozens or hundreds of function instances may spin up simultaneously, each trying to open its own database connection, you can quickly hit this limit and get connection refused errors. Neon's built-in PgBouncer connection pooler solves this by maintaining a pool of connections to the database and multiplexing many application requests through them.
To use the pooler, you simply use the pooled connection string (which ends in -pooler.neon.tech)
for your application queries. For Prisma migrations and schema introspection, you must use the direct
(non-pooled) connection string — migrations require a persistent connection that the pooler can't provide. This
is why the Prisma schema uses both url (pooled, for queries) and directUrl (direct,
for migrations).
# .env.local
# For application queries (Server Components, API Routes, Server Actions)
DATABASE_URL="postgresql://USER:PASS@HOST-pooler.neon.tech/DBNAME?sslmode=require&pgbouncer=true"
# For Prisma migrate and introspect (direct connection only)
DATABASE_URL_UNPOOLED="postgresql://USER:PASS@HOST.neon.tech/DBNAME?sslmode=require"
Vercel is the most popular deployment platform for Next.js and has a first-class Neon integration that makes it trivial to connect your database to your deployed application. Once connected, Vercel automatically injects the correct Neon environment variables into your deployment.
DATABASE_URL, DATABASE_URL_UNPOOLED, and other
Neon variables in your project's environment settings.DATABASE_URL with your pooled connection string.DATABASE_URL_UNPOOLED with your direct connection string.Add a build command to your package.json that runs migrations before the Next.js build:
"scripts": {
"build": "prisma migrate deploy && next build",
"dev": "next dev",
"start": "next start"
}
prisma migrate deploy applies any pending migrations without prompting for input — safe for
CI/CD pipelines.
The Neon driver's tagged template literal syntax automatically parameterizes any JavaScript value you interpolate into a query. This makes it impossible to accidentally create a SQL injection vulnerability through interpolation — the driver always sends values as separate parameters, never concatenated into the query string.
// “& SAFE — driver parameterizes userInput automatically
const userId = req.params.id;
const rows = await sql`SELECT * FROM users WHERE id = ${userId}`;
// ’R NEVER DO THIS — string concatenation bypasses parameterization
const rows = await sql`SELECT * FROM users WHERE id = ` + userId; // SQL injection risk!
// “& SAFE — multiple parameters, all automatically escaped
const rows = await sql`
SELECT * FROM posts
WHERE author_id = ${authorId}
AND published = ${true}
AND created_at > ${startDate}
LIMIT ${limit}
`;
When you need to perform multiple related writes that must all succeed or all fail together — for example,
creating an order and deducting inventory simultaneously — you need a database transaction. The Neon serverless
driver supports transactions using the transaction method.
import { neon } from '@neondatabase/serverless';
async function placeOrder(userId: number, productId: number, quantity: number) {
const sql = neon(process.env.DATABASE_URL!);
// All queries inside the array run in a single transaction
const [order] = await sql.transaction([
sql`
INSERT INTO orders (user_id, product_id, quantity, created_at)
VALUES (${userId}, ${productId}, ${quantity}, NOW())
RETURNING id
`,
sql`
UPDATE products
SET stock = stock - ${quantity}
WHERE id = ${productId} AND stock >= ${quantity}
`,
]);
return order;
}
Building search and filter features often requires queries where the WHERE clause changes based on user input. The Neon driver lets you compose queries conditionally:
import { neon, sql as neonSql } from '@neondatabase/serverless';
async function searchPosts(filters: {
authorId?: number;
published?: boolean;
searchTerm?: string;
}) {
const sql = neon(process.env.DATABASE_URL!);
const rows = await sql`
SELECT id, title, body, created_at
FROM posts
WHERE TRUE
${filters.authorId ? neonSql`AND author_id = ${filters.authorId}` : neonSql``}
${filters.published !== undefined ? neonSql`AND published = ${filters.published}` : neonSql``}
${filters.searchTerm ? neonSql`AND title ILIKE ${'%' + filters.searchTerm + '%'}` : neonSql``}
ORDER BY created_at DESC
`;
return rows;
}
Database security is not just about preventing SQL injection. Here is a full checklist of security practices to apply when connecting Next.js to Neon in production.
sslmode=require parameter in your connection string to enforce encrypted
connections to the database.prisma.$executeRawUnsafe() with user-provided values — prefer
parameterized raw queries with prisma.$executeRaw tagged templates.One of Neon's most powerful and distinctive features is database branching. Just like Git branches let you work on code changes in isolation without affecting the main codebase, Neon branches let you create isolated copies of your database for development, testing, or feature work — without duplicating the actual data storage.
Branches are copy-on-write, which means creating a branch is instant and nearly free regardless of database size. Changes you make on a branch do not affect the parent branch. When you are done, you can merge changes back or simply delete the branch.
In the Neon dashboard, click Branches — New Branch. Name it
development or after your feature (e.g., feature/user-profiles). Neon will create a
branch with its own connection string that points to an isolated copy of the data from the parent branch.
Use the branch's connection string in your .env.local during development, so your local
experiments never touch production data.
The Neon Vercel integration can automatically create a new database branch for each Vercel preview deployment. When a PR is opened, Vercel creates a preview deployment and Neon creates a matching database branch. The preview app talks to its own isolated database, making it safe to test schema changes and data migrations without risking production.
Enable this in the Neon integration settings in your Vercel dashboard by turning on Preview Branches.
wsInstall the missing peer dependency: npm install ws and npm install -D @types/ws.
This is required when using the Neon driver with Prisma in a Node.js environment.
DATABASE_URL is undefinedMake sure your .env.local file is in the root of your project (not inside /app or
/src). Also verify you restarted the dev server after adding the variable — Next.js reads
.env.local at startup.
Ensure your connection string includes ?sslmode=require at the end. Neon requires SSL for all
connections. If you are using Prisma, also confirm your schema.prisma datasource URL includes
the SSL parameter.
You are likely using the pooled connection string for migrations. Switch to the direct (non-pooled)
connection string for prisma migrate dev and prisma migrate deploy. Pooler
connections do not support the persistent connection that migrations require.
Neon's serverless compute scales to zero when inactive. The first query after a period of inactivity triggers a "cold start" that adds 100—500ms. For latency-sensitive applications, consider upgrading to Neon's paid plan which keeps compute always-on.
Run npx prisma generate after every schema change to regenerate the Prisma client and its
TypeScript types. If types are still stale, delete the node_modules/.prisma folder and
regenerate.
Yes. The @neondatabase/serverless driver is compatible with the Vercel Edge Runtime and Cloudflare
Workers. Use the HTTP transport mode (the default when webSocketConstructor is not set) for edge
environments. Edge compatibility makes it possible to run database queries at the CDN level for ultra-low
latency global applications.
Neon runs standard PostgreSQL, so nearly all PostgreSQL features work as expected: full-text search, JSONB
columns, window functions, CTEs, foreign keys, indexes, triggers, and extensions like pgvector for
AI embeddings or uuid-ossp for UUID generation. The main limitation is that some PostgreSQL
superuser features are restricted, which is typical of hosted database services.
Neon automatically backs up your data continuously using write-ahead log (WAL) archiving. You can restore your database to any point in time within your retention window (7 days on the free tier, longer on paid plans) using the Neon dashboard's point-in-time restore feature. You can also use Neon's branching as an instant snapshot mechanism — create a branch before a risky migration to have a zero-downtime rollback option.
Yes. Use pg_dump to export your existing database and psql or pg_restore
to import it into Neon. Alternatively, use Neon's logical replication feature to migrate live data with minimal
downtime from an existing PostgreSQL database.
The raw Neon driver is simpler and has less overhead — great for simple applications, scripts, or when you want full control over your SQL. Prisma adds type safety (auto-generated TypeScript types from your schema), a migration system, a query builder, and better tooling for complex data models. For production applications with complex schemas and multiple developers, Prisma's structure and type safety usually justify the added setup complexity. For quick prototypes or read-heavy dashboards with simple queries, the raw driver is faster to get started with.
Yes. Drizzle ORM has official support for Neon and is an increasingly popular alternative to Prisma in the
Next.js ecosystem. Drizzle is lighter-weight, keeps SQL more explicit, and has excellent TypeScript inference.
To use Drizzle with Neon, install drizzle-orm and drizzle-kit alongside
@neondatabase/serverless and follow Drizzle's Neon integration guide in their documentation.
Connecting Next.js to Neon is one of the most streamlined database setups available today. In under an hour, you have a fully serverless PostgreSQL database, connected to your Next.js app, with type-safe queries through Prisma, connection pooling for production scale, and branch-based environments for safe development and previews.
The stack you have built in this guide — Next.js App Router + Neon + Prisma — scales from a zero-cost side project to a production SaaS application without requiring any infrastructure changes. Neon's serverless nature means you pay for what you use, and its branching capabilities bring modern development workflows to your database layer for the first time.
Start with the core connection, ship your first data-driven page, and expand from there. The patterns covered in this guide — Server Components, Server Actions, API Routes, migrations, transactions, and security hardening — will carry you through the full lifecycle of a production application.