Neon
Neon is a Postgres reached through DATABASE_URL, like any other. What sets it apart is its serverless drivers.
Drivers
Neon's serverless drivers (@neondatabase/serverless, through drizzle-orm/neon-http or drizzle-orm/neon-serverless) speak HTTP and WebSocket to Neon's proxy, not the Postgres wire protocol. That is what makes them fast from a serverless function, and why they cannot reach the development socket, which speaks the wire protocol only.
Use node-postgres in development and switch on import.meta.dev, which the bundler replaces with a constant, so only the production driver is in the build:
import { drizzle as drizzleNeon } from 'drizzle-orm/neon-http'
import { drizzle as drizzlePg } from 'drizzle-orm/node-postgres'
import { relations } from '../database/relations'
export function useDB() {
const url = process.env.DATABASE_URL!
return import.meta.dev ? drizzlePg(url, { relations }) : drizzleNeon(url, { relations })
}
Both drivers return the same Drizzle API, so the routes do not know which one they got. If you do not need the serverless drivers, connect to Neon with pg in production too, through its standard connection string, and skip the switch.
Migrations
Neon does not apply migrations: you do. Apply the same files init applies locally, with the production DATABASE_URL, from CI or the build command, as for any Postgres:
- run: pnpm drizzle-kit migrate
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
Give it Neon's direct (unpooled) connection string, as Neon recommends for schema changes: its pooler runs in transaction mode, which session-level statements in a migration can trip on.
Branches
Neon branches a database in seconds, which pairs well with preview deployments: create a branch per pull request in CI, apply the migrations to it, and hand its URL to the preview as DATABASE_URL. Production gets them once the branch is merged.