Every application eventually needs persistent storage, and the choice you make in week one shapes your architecture for years. In 2026, the decision surface is smaller than it looks: Postgres covers the vast majority of use cases, and the managed-service options make operational complexity nearly zero. Here is how to make the right call and set it up correctly.
What changed in 2026
- pgvector 0.7+ is production-grade — storing and querying embeddings in Postgres is now a legitimate alternative to a dedicated vector database for most workloads.
- Neon branching is widely adopted — database branches for PR previews, same pattern as Vercel branch deployments.
- Drizzle ORM overtook Prisma in weekly downloads — its lightweight, SQL-close API and zero-runtime migration mode resonated with serverless teams.
- PlanetScale dropped its free tier but its branching workflow influenced every major managed Postgres provider.
- SQLite on the server is real — Turso (libSQL) and Cloudflare D1 make SQLite viable for edge-deployed apps.
Choosing your database
| Use case |
Best choice |
Why |
| General web app |
Postgres |
Best ecosystem, extensions, JSONB |
| Edge-deployed app |
SQLite (Turso / D1) |
Low latency, no connection pool |
| Caching / sessions |
Redis (Upstash) |
Sub-millisecond reads |
| Time-series metrics |
TimescaleDB |
Postgres extension, hypertables |
| Vector search |
pgvector in Postgres |
No second DB needed |
| Document store |
Postgres JSONB |
Avoid MongoDB for new projects |
The "just use Postgres" answer is right ~80% of the time.
Managed service comparison
| Service |
Engine |
Free tier |
Branching |
Edge-native |
| Neon |
Postgres 16 |
Yes (generous) |
Yes |
Serverless driver |
| Supabase |
Postgres 16 |
Yes |
No |
Partial |
| PlanetScale |
MySQL 8 |
No |
Yes |
No |
| Railway |
Postgres / MySQL |
Limited |
No |
No |
| Turso |
SQLite (libSQL) |
Yes |
Yes |
Yes |
For a Next.js or SvelteKit app on Vercel, Neon is the most frictionless choice.
Setting up Postgres with Neon + Drizzle
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit
// src/db/schema.ts
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
export const tasks = pgTable("tasks", {
id: serial("id").primaryKey(),
title: text("title").notNull(),
status: text("status").notNull().default("todo"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
// src/db/index.ts
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
// drizzle.config.json
{
"schema": "./src/db/schema.ts",
"out": "./drizzle",
"dialect": "postgresql",
"dbCredentials": { "url": "$DATABASE_URL" }
}
npx drizzle-kit generate # create migration SQL files
npx drizzle-kit migrate # apply to database
Connection pooling for serverless
Serverless functions (Vercel, Lambda) create a new database connection per invocation. Without pooling, you exhaust Postgres's connection limit (~100 for a small instance) almost immediately under load.
Solutions:
- Neon serverless driver (
@neondatabase/serverless) — uses HTTP instead of persistent TCP; no pooling needed.
- PgBouncer in transaction mode — sits between your app and Postgres, reusing connections.
- Prisma Accelerate — Prisma's managed connection pool and query cache.
// Neon HTTP driver — no pool exhaustion on serverless
import { neon } from "@neondatabase/serverless";
const sql = neon(process.env.DATABASE_URL!);
const rows = await sql`SELECT * FROM tasks WHERE status = ${"todo"}`;
Migrations best practice
Never ALTER TABLE in production manually. A migration workflow:
- Edit
schema.ts — add or change a column.
- Generate —
drizzle-kit generate creates a timestamped SQL file.
- Review — read the SQL; migrations that drop columns need a multi-step rollout.
- Apply — run in CI before deploying the new app version, not after.
For Prisma users, the equivalent is prisma migrate dev (development) and prisma migrate deploy (CI/CD).
How to pick your ORM
| ORM |
Style |
Performance |
TypeScript |
Best for |
| Drizzle |
SQL-close |
Excellent |
Excellent |
Serverless, SQL fans |
| Prisma 6 |
Abstracted |
Good |
Excellent |
Rapid prototyping |
| Kysely |
Query builder |
Excellent |
Excellent |
Type-safe raw SQL |
| No ORM (postgres.js) |
Raw SQL |
Best |
Manual |
Simple scripts |
Common mistakes
Using connection strings with sslmode=disable in production. Always use sslmode=require for cloud databases. A TLS-free connection is a security incident waiting to happen.
Running migrations inside the application on startup. This creates race conditions in multi-instance deployments. Run migrations as a separate CI/CD step.
Storing large blobs in Postgres. Files over ~1 MB should go to object storage (S3-compatible); Postgres BYTEA is not a CDN.
Not indexing foreign keys. Postgres does not auto-index foreign key columns. An unindexed FK causes full table scans on every join.
What to skip
- MongoDB for new projects without a specific document-store use case — Postgres JSONB handles most flexible-schema needs with better query tools.
- Self-hosted Postgres before you have a DBA — backups, failover, and vacuuming require expertise; pay the managed service tax.
- ORM-generated migrations you do not review — auto-generated
DROP COLUMN migrations will ruin your day if applied to a busy production table.
FAQ
How do I back up my Neon/Supabase database?
Managed services handle backups automatically. For additional assurance, run pg_dump in a cron job and store the output in object storage.
Should I use UUIDs or serial IDs as primary keys?
UUIDs (gen_random_uuid()) are better for distributed systems and prevent enumeration. Serial integers are simpler and slightly faster for single-DB apps. ULID or UUID v7 are good compromises in 2026 (sortable, globally unique).
Can I use Postgres for a queue?
Yes — SELECT ... FOR UPDATE SKIP LOCKED implements a reliable queue. For high throughput (>1k jobs/sec), reach for a dedicated queue (BullMQ, Inngest).
Is SQLite worth considering for a new web app?
For edge-deployed apps (Cloudflare Workers, Deno Deploy), SQLite via Turso or D1 is a serious choice. For a traditional server, Postgres is still the safer default.
Where to go next