Postgres in Next.js
The database only on the server, Neon and pooling, caching by kind of data, seeding and migrations.
Updated
What connecting Next.js to a database means
In Next.js, the database is accessed only on the server: in Server Components, Server Actions and Route Handlers. A component in the browser never talks to the database directly — it always goes through your server code, which checks who's asking and for what.
This app is exactly that kind of project: Next.js 16 + Postgres on Neon.
"Serverless" Postgres — why Neon, Supabase, Vercel Postgres
A classic database keeps connections open permanently. Serverless functions (Vercel) start and stop often — each one would open new connections and exhaust the limit.
| Solution | How it solves it |
|---|---|
an HTTP driver (@neondatabase/serverless) |
each query is an HTTP request — no connection to keep |
connection pooling (PgBouncer, a URL with -pooler) |
a middleman reuses a few real connections |
| scale to zero | the database stops when it isn't used (a low cost for personal projects) |
| branching (Neon) | an instant copy of the database for every preview / PR |
The structure in the project
.env.localDATABASE_URL — never in Gitsrc/shared/api/db.tsthe client, with import 'server-only'
entities/lesson/api/lesson-api.tsqueries with 'use cache' + cacheTag
progress/api/progress-api.tsper-user reads (dynamic)save-progress.tsthe write Server Action
db/schema.sqlthe table structure
scripts/seed.tsinitial data
// shared/api/db.ts
import 'server-only'
import { neon } from '@neondatabase/serverless'
export const sql = neon(process.env.DATABASE_URL!)server-only makes the build fail if the module ever ends up in a Client Component.
Reading, writing, caching — the full pattern
| Operation | Where | Cache |
|---|---|---|
| public data (lessons, products) | a function in entities/*/api |
'use cache' + cacheLife + cacheTag |
| per-user data (progress, a cart) | a function that reads cookies() / the session |
dynamic, inside <Suspense> |
| writing | a Server Action: auth → Zod validation → query → updateTag / refresh() |
invalidation |
All three are implemented in this app — see Caching and Server Actions.
Seeding and test data
A seed fills the database with initial data (the lesson content here comes from npm run seed). The rules: a seed is idempotent (running it twice gives the same result — INSERT ... ON CONFLICT DO UPDATE), and it isn't run on production with test data.
A production checklist
DATABASE_URLset in Vercel separately for Production and Preview (ideally, a separate Neon branch for previews).- The pooler URL for serverless functions.
- Migrations run before deploying the code that depends on them.
- Indexes on the columns used in
WHEREandJOIN. - Backups / point-in-time restore enabled.
Summary
- The database only on the server; the DB client with
server-only. - Serverless → an HTTP driver or pooling; Neon also offers branching for previews.
- Public →
'use cache'; per user → dynamic inside Suspense; writes → a Server Action + invalidation.