
Netlify Database
- 1.3k installs
- 31 repo stars
- Updated August 4, 2026
- netlify/context-and-tools
netlify-database is an official Netlify agent skill that provisions and operates GA managed Postgres on Netlify sites for developers who need relational data without external Neon wiring.
About
netlify-database is an official Netlify agent skill for GA managed Postgres built into the Netlify platform. It teaches agents to run netlify database init, install @netlify/database, use drizzle-orm@beta and drizzle-kit@beta for the drizzle-orm/netlify-db adapter, and commit timestamp-prefix migrations under netlify/database/migrations/. Netlify CLI 26.0.0+ exposes the full netlify database surface with --json output; deploys auto-provision databases, apply migrations on hosted branches, and fork isolated preview branches from production. The skill forbids psql, drizzle-kit push, and side-channel DDL against NETLIFY_DB_URL while documenting local dev via netlify dev and separation from Netlify Blobs for files. Reference guides cover migrations, CLI commands, local development, legacy @netlify/neon extension migration, and provider switching. Developers reach for netlify-database when adding users, orders, sessions, or other relational records to Netlify Functions or edge apps.
- Netlify DB provisioning
- Connection and env configuration
- Schema and migration guidance
- Query patterns from functions
- Deploy-linked data persistence
Netlify Database by the numbers
- 1,297 all-time installs (skills.sh)
- +128 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #73 of 911 Databases skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/netlify/context-and-tools --skill netlify-databaseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.3k |
|---|---|
| repo stars | ★ 31 |
| Last updated | August 4, 2026 |
| Repository | netlify/context-and-tools ↗ |
How do you add Postgres to a Netlify site?
Provision and use Netlify database services from app code, including schema setup, connections, queries, and deploy-time data layer configuration.
Who is it for?
Backend developers shipping relational data on Netlify who want preview-branch Postgres without manual Neon account setup.
Skip if: Teams storing only static assets or binary uploads that belong in Netlify Blobs rather than a relational database.
When should I use this skill?
A Netlify project needs dynamic structured data, schema migrations, or deploy-preview database branching.
What you get
drizzle.config.ts, migration SQL files, @netlify/database dependency, and deploy-ready schema under netlify/database/migrations/
- drizzle.config.ts
- SQL migration files
- NETLIFY_DB_URL query layer
By the numbers
- Requires Netlify CLI 26.0.0+ for netlify database commands
- Includes 5 reference guides for migrations, CLI, local dev, and legacy extension migration
- Uses drizzle-orm@beta and drizzle-kit@beta for the Netlify Database adapter
Files
Netlify Database
Netlify Database is the managed Postgres product built into the Netlify platform. It is GA and is the default choice for any dynamic data in a Netlify project.
Install @netlify/database and Netlify auto-provisions a Postgres database for the site at deploy time. Each deploy preview gets its own isolated branch forked from production data. No Neon account, connection-string wiring, or claim flow — the database is a first-class Netlify primitive.
Database vs Blobs
Use Netlify Database for anything dynamic:
- Any user-generated or app-generated records (posts, comments, orders, sessions, audit logs)
- Structured data that will grow, be queried, or be joined
- Key-value-style data read or written by application code at runtime
Use Netlify Blobs only for file and asset storage: images, documents, exports, uploads, cached binary artifacts. Do not use Blobs as a dynamic data store — reach for Database instead. See netlify-blobs/SKILL.md.
Before you build
If the prompt didn't already specify, ask the user a few short questions before scaffolding any database code — answers shape the schema, the seed data, and the query layer:
- What entities does the app need? (Users, posts, orders, sessions — drives the schema in
db/schema.ts.) - Any seed data for the prototype? (Test rows, default roles, sample content — these become a DML migration, not ad-hoc
INSERTs against production.) - Drizzle (recommended) or native driver? (Drizzle for type-safe queries and generated migrations; native for raw SQL or a different query builder like Kysely.)
If you don't have preferences here, tell me roughly what the app does and I'll pick sensible defaults — typically Drizzle with timestamp-prefix migrations and a starter schema for the entities the prompt implies.
CRITICAL: Install Drizzle from the @beta dist-tag
The Netlify Database adapter for Drizzle ORM currently only exists on the beta release line of drizzle-orm. Install both drizzle-orm and drizzle-kit from the @beta dist-tag:
npm install drizzle-orm@beta
npm install -D drizzle-kit@betaThe default latest versions do not include the drizzle-orm/netlify-db adapter and will fail. If drizzle-kit generate errors about being outdated, or the drizzle-orm/netlify-db import fails to resolve, the install is missing @beta.
The @beta tag only affects the installed version — imports are written as drizzle-orm, drizzle-orm/pg-core, and drizzle-orm/netlify-db without modification.
CRITICAL: Use the Netlify CLI for database operations
The CLI ships a complete database surface under netlify database (alias: netlify db) that replaces hand-rolled scripts and direct API/UI work. Reach for these commands first before writing custom tooling. Requires Netlify CLI 26.0.0+ — if a netlify database subcommand isn't recognized, run npm install -g netlify-cli@latest.
The corollary: never go around the CLI, even for read-only operations. Specifically:
- Do not run `psql`, `pg_dump`, or any other raw Postgres client against a Netlify-hosted database, even for "harmless"
SELECTs. Usenetlify database connect --query "..."instead. - Do not curl `https://api.netlify.com/...` to manage the database.
- Do not read auth tokens out of
~/Library/Preferences/netlify/config.json(or anywhere on disk) to authenticate side-channel calls. - Do not use `netlify api <method>` as a recovery hatch when a deploy fails to provision the database. The supported recovery is documented under If the first deploy fails to provision the database.
Wandering off the documented surface is how agents end up touching the wrong database, deleting the wrong resource, or leaking credentials. If the documented happy path doesn't work, surface the failure to the user with context — see When something fails, surface and stop below.
Full reference is in Netlify CLI commands below.
When something fails, surface and stop
When a netlify command fails, a deploy fails to provision the database, or any documented happy path errors out, the right action is to report the failure to the user with the deploy log URL, the exact error, and the affected site/branch — and stop. Do not invent recovery commands or escalate to lower-level tools.
Specific failure modes (like the first-deploy 401) get their own playbooks below. The general rule applies everywhere: a stuck agent surfacing context is far safer than a "helpful" agent that wanders off and deletes the wrong database.
CRITICAL: Never apply migrations to a Netlify-hosted database
The only database you ever apply migrations to yourself is the local development DB. For any Netlify-hosted database — production or a deploy preview branch — Netlify applies migrations automatically during the deploy. Your job is to create migration files and commit them. The deploy does the rest.
This means:
- Use
netlify database migrations applyfor the local DB. Do NOT rundrizzle-kit migrateagainstNETLIFY_DB_URLin any context. - Do NOT run
drizzle-kit pushat all. Generate a migration and let the deploy apply it. - Do NOT run raw DDL (
CREATE,ALTER,DROP,TRUNCATE) vianetlify database connect,psql, or any other direct connection. Schema changes go through migration files; out-of-band DDL drifts the migration history from the actual schema. - Do NOT export
NETLIFY_DB_URLfrom a preview or production context and run a client against it. Migrations drift the moment anything touches the schema out-of-band.
The one documented exception is a one-time data import during a provider switch — see references/migration-from-extension.md. Outside that specific flow, the rule is absolute: schema changes go through migration files, migration files get applied by the deploy.
Not the legacy "Netlify DB" extension
The older Netlify DB extension (Beta) is deprecated. It is a separate product and must not be confused with Netlify Database. Do not recommend or install it for new projects.
| Netlify Database (GA, recommended) | Netlify DB extension (deprecated Beta) | |
|---|---|---|
| Package | @netlify/database | @netlify/neon |
| Env var | NETLIFY_DB_URL | NETLIFY_DATABASE_URL |
| Setup | netlify database init or install the package — auto-provisioned at deploy | Historically netlify db init on older CLI versions, with a claim into the user's Neon account; that flow is no longer reachable from the current CLI |
| Status | GA | Deprecated; new creation blocked as of April 2026 |
If an existing project is already using the @netlify/neon extension, keep it working and encourage the user to switch. See references/legacy-extension.md for recognition and coexistence, and references/migration-from-extension.md for the full switching process (also covers switching from other external Postgres providers).
Provisioning
The fastest path is netlify database init — an interactive setup that installs @netlify/database, lets the user pick Drizzle or raw SQL, writes drizzle.config.ts if needed, scaffolds a starter migration, applies it locally, and runs a sample query end-to-end:
netlify database init # interactive
netlify database init --yes # accept defaults — for CI/agentsIf you'd rather wire things up by hand, install the package directly:
npm install @netlify/databaseEither way, the presence of @netlify/database in the dependency tree triggers provisioning on the next deploy. A database can also be created manually from the Netlify UI before first deploy, but the package + deploy path is the supported automation flow.
Provisioning workflow: preview-first
The supported inner loop is preview-first, not --prod-first:
1. First deploy: `netlify deploy` (no --prod). This provisions the database if needed, applies any pending migrations to the production branch, and produces a draft URL. Verify the deploy log shows Netlify Database setup completed in <n>s (and, if migrations exist, Loading migrations from netlify/database/migrations directory) before continuing. 2. User verifies on the draft URL — and completes any dashboard-only setup along the way (e.g., enabling Identity if the project also uses it; see netlify-identity/SKILL.md). 3. Promote: `netlify deploy --prod`.
Why preview-first matters: the preview deploy provisions the database and applies migrations exactly the way the production deploy will, so a failure during preview is recoverable without prod ever entering a half-configured state. --prod-first works in the happy case but is harder to recover from when something goes wrong.
If the first deploy fails to provision the database
Symptom: the build (or the Netlify Database setup extension inside the build) fails with a 401 Access Denied on createSiteDatabase, typically on the very first deploy of a brand-new site. The deploy log shows the failure inside the extension's setup step.
If the failure happened on `netlify deploy --prod` as the very first deploy, the first thing to try is the supported preview-first flow — run netlify deploy (no --prod). The failure has only been observed on --prod-first attempts on brand-new sites.
If a preview deploy also fails — or the original failure was already on a preview — report the failure to the user and stop. Do not work around it. Specifically, do not:
- Curl
https://api.netlify.com/...directly - Run
netlify api createSiteDatabase(or any othernetlify apicall to manually create what the platform was supposed to provision) - Pull auth tokens out of
~/Library/Preferences/netlify/config.json - Connect via
psqlto "check on things"
The recovery is to give the user the deploy log URL, the site URL, and the exact error, and let them decide what to do next (file a support issue, recreate the site fresh, switch teams, etc.). Wandering off the happy path is how agents end up deleting the wrong resource — being stuck and clear is much safer than being "helpful" with side-channel calls.
Drizzle ORM (recommended path)
Drizzle is the recommended way to work with Netlify Database. Prefer Drizzle over writing raw SQL or hand-editing migration files — manual migrations are an edge case (see references/migrations.md).
Install
npm install @netlify/database drizzle-orm@beta
npm install -D drizzle-kit@betaSchema file
Create db/schema.ts. Define all tables here using Drizzle's schema builder.
// db/schema.ts
import { boolean, pgTable, serial, text, timestamp, varchar } from "drizzle-orm/pg-core";
export const items = pgTable("items", {
id: serial().primaryKey(),
title: varchar({ length: 255 }).notNull(),
description: text(),
isActive: boolean("is_active").notNull().default(true),
createdAt: timestamp("created_at").defaultNow(),
updatedAt: timestamp("updated_at").defaultNow(),
});
export type Item = typeof items.$inferSelect;
export type NewItem = typeof items.$inferInsert;Use snake_case strings for column names ("is_active", "created_at") to match Postgres conventions. Drizzle variable names can be camelCase.
Drizzle client
Create db/index.ts. The adapter on drizzle-orm/netlify-db picks the right driver for the runtime automatically.
// db/index.ts
import { drizzle } from "drizzle-orm/netlify-db";
import * as schema from "./schema";
export const db = drizzle({ schema });The connection is configured automatically — no connection string needed. If your project uses native ESM with .js extensions on relative imports (from "./schema.js"), keep that style consistent here.
Drizzle Kit config
Create drizzle.config.ts at the project root. Set out to netlify/database/migrations — that's the directory the deploy applies migrations from:
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: "postgresql",
schema: "./db/schema.ts",
out: "netlify/database/migrations",
});Package scripts
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "netlify database migrations apply"
}
}db:generatewrites a new migration file undernetlify/database/migrations/from the current schema.db:migrateapplies pending migrations to the local development database only, via the CLI. Hosted migrations (preview branches, production) are applied by the deploy — never by this script.
Schema-change workflow
1. Edit db/schema.ts. 2. npm run db:generate — writes a new file into netlify/database/migrations/. 3. Review the SQL. 4. npm run db:migrate — applies it to the local development DB for testing. 5. Commit the schema change and migration file together and push. The deploy applies the migration to the preview branch, then to production on publish.
Query patterns
import { db } from "./db";
import { items } from "./db/schema";
import { eq, desc } from "drizzle-orm";
// Select all
const all = await db.select().from(items);
// Select with condition
const [one] = await db.select().from(items).where(eq(items.id, id)).limit(1);
// Ordering and limit
const recent = await db.select().from(items).orderBy(desc(items.createdAt)).limit(10);
// Insert
const [created] = await db.insert(items).values({ title: "New" }).returning();
// Update
const [updated] = await db.update(items).set({ title: "Updated" }).where(eq(items.id, id)).returning();
// Delete
await db.delete(items).where(eq(items.id, id));Full migration workflow, expand-and-contract for breaking schema changes, and production DML migrations are in references/migrations.md.
Native driver (when Drizzle isn't a fit)
When a project wants raw SQL, uses a different query builder (Kysely, etc.), or has a library that needs a pg.Pool, use the native driver exposed by @netlify/database.
npm install @netlify/databaseimport { getDatabase } from "@netlify/database";
const db = getDatabase();
// Tagged template — parameters are safely bound, not interpolated
const users = await db.sql`SELECT * FROM users WHERE active = ${true}`;
// Insert with RETURNING
const [user] = await db.sql`
INSERT INTO users (name, email)
VALUES (${name}, ${email})
RETURNING *
`;
// Bulk insert
const rows = db.sql.values([
["Ada", "ada@example.com"],
["Bob", "bob@example.com"],
]);
await db.sql`INSERT INTO users (name, email) VALUES ${rows}`;Transactions go through db.pool so BEGIN, the queries, and COMMIT/ROLLBACK all run on the same connection:
import { getDatabase } from "@netlify/database";
const db = getDatabase();
const client = await db.pool.connect();
try {
await client.query("BEGIN");
await client.query("INSERT INTO users (name, email) VALUES ($1, $2)", [name, email]);
await client.query("INSERT INTO posts (author_id, title) VALUES ($1, $2)", [id, title]);
await client.query("COMMIT");
} catch (e) {
await client.query("ROLLBACK");
throw e;
} finally {
client.release();
}For third-party tools that need a raw connection string, import getConnectionString from @netlify/database — but prefer getDatabase() for application code.
Manual migrations
With the native driver, scaffold migration files via the CLI:
netlify database migrations new -d "create users table"This creates netlify/database/migrations/<prefix>_<slug>/migration.sql and prompts for the numbering scheme if it can't be detected from existing files. Open the file and write the SQL. The CLI auto-detects an existing scheme; for new projects it'll ask — choose timestamp unless you have a reason not to.
You can also write the file by hand if you prefer. Two layouts are supported:
- Flat:
netlify/database/migrations/<prefix>_<slug>.sql - Subdirectory:
netlify/database/migrations/<prefix>_<slug>/migration.sql(whatmigrations newproduces)
In both, <prefix> is digits (timestamp like 20260417143022 or sequential like 0001) and <slug> is lowercase letters, numbers, hyphens, or underscores. Files apply in lexicographic order. See references/migrations.md.
Once a migration has been applied to any database, never modify it — roll forward with a new migration instead.
Connection — don't reach for the env var
NETLIFY_DB_URL is set automatically across builds, functions, edge functions, and local dev. Use the getDatabase() / getConnectionString() helpers above rather than reading it directly — only reach for the raw env var for third-party tools that demand a bare string.
NETLIFY_DB_URL is intentionally different from the legacy extension's NETLIFY_DATABASE_URL. The two coexist so a project mid-migration doesn't break. Don't rename between them.
Preview branching
Each deploy preview runs against its own database branch forked from production data. Schema and data changes in a preview do not affect production until the branch is merged and published. This means:
- Migrations run against the preview branch first — failures fail the preview, not production.
- Ad-hoc edits in a preview (via the Netlify UI data browser or a direct client) do not propagate to production. Always express production changes as migrations.
Production data changes — write a DML migration
When a user asks for data changes that should land in production (seed data, backfills, one-off cleanups, CSV imports), do not connect to the production database and run queries. Generate a DML migration in netlify/database/migrations/ (SQL INSERT/UPDATE/DELETE, or a Drizzle-generated equivalent). Tell the user you created a data migration and that merging to production will apply it. Let them verify in the preview branch first.
If the request is ambiguous ("update this record"), confirm that the user wants a production migration rather than a preview-only edit. See references/migrations.md.
Netlify CLI commands for Netlify Database
The CLI ships a complete database surface under netlify database (alias: netlify db). Requires CLI 26.0.0+. Most commands accept --json for machine-readable output — useful when scripting or reading results from an agent.
Full per-command reference — init, status, connect, migrations new / apply / pull / reset, and reset — is in references/cli-commands.md. The one rule that applies across all of them: never run DDL (`CREATE`/`ALTER`/`DROP`/`TRUNCATE`) through `connect`, `psql`, or any direct connection — schema changes go through migration files.
Iterating on migrations
When a migration you generated needs to change, what you do depends on whether it's been applied anywhere yet:
- Already applied to any database (local dev DB, a preview branch, or production) → treat as immutable. Roll forward with a new migration that applies the correction.
- Only on disk (not yet applied anywhere) → don't edit the SQL or snapshot files by hand. Run
netlify database migrations reset, updatedb/schema.ts, then re-runnpm run db:generate. Hand-editing desyncs Drizzle Kit's internal state and tends to produce broken migrations on the next generate.
Local development
netlify dev runs the project against a local Postgres-compatible database — no remote connection, no risk of touching production. Use netlify database migrations apply to apply pending migrations locally, netlify database connect to query, and netlify database reset to wipe and replay. See references/local-dev.md.
Common mistakes
1. Forgetting the `@beta` dist-tag. drizzle-orm and drizzle-kit must be installed as @beta. The latest releases lack the drizzle-orm/netlify-db adapter. 2. Wrong migration output directory. Drizzle Kit defaults to drizzle/. Set out: "netlify/database/migrations" in drizzle.config.ts — migrations outside that directory are not applied by the deploy. 3. Writing raw `CREATE TABLE` when using Drizzle. The schema file is the source of truth. Define tables in db/schema.ts and generate migrations. 4. Running `drizzle-kit migrate` or `push` against a hosted DB. Never. The deploy applies migrations. For local, use netlify database migrations apply instead. 5. Using `netlify database connect` to change schema. Schema changes go through migration files — never DDL through connect or any direct connection. 6. Misunderstanding `netlify database migrations reset`. It only deletes unapplied files. It cannot undo an applied migration — for that, roll forward with a new migration. 7. Assuming `netlify dev` applies migrations automatically. It doesn't — only the deploy does. Run netlify database migrations apply locally yourself.
Netlify CLI commands for Netlify Database
The CLI ships a complete database surface under netlify database (alias: netlify db). Requires CLI 26.0.0+. Most commands accept --json for machine-readable output — useful when scripting or reading results from an agent.
netlify database init
Interactive bootstrap: installs @netlify/database (and Drizzle if chosen), writes drizzle.config.ts, scaffolds and applies a starter migration, and runs a sample query. Use --yes for non-interactive mode.
netlify database status
Reports whether the database is enabled, whether @netlify/database is installed, the connection string for the active branch, and the applied/pending/missing/out-of-order migrations. Defaults to the local development database — pass --branch <name> to target a remote preview or production branch.
netlify database status # local
netlify database status --branch my-feature # remote branch
netlify database status --json
netlify database status --show-credentials # include username/password in connection stringnetlify database connect
Connects to the database. Defaults to an interactive REPL — for agent and script use, always pass --query for one-shot execution:
# List tables
netlify database connect --query "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'"
# Inspect columns
netlify database connect --query "SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = 'items'"
# JSON output
netlify database connect --query "SELECT * FROM items LIMIT 10" --json
# Get connection details only (no query)
netlify database connect --jsonNever run DDL (`CREATE`, `ALTER`, `DROP`, `TRUNCATE`) through `netlify database connect`, `psql`, or any other direct connection. Schema changes go through migration files — out-of-band DDL drifts the migration history from the actual schema.
netlify database migrations new
Scaffolds a new migration file as netlify/database/migrations/<prefix>_<slug>/migration.sql. Auto-detects the numbering scheme from existing files; prompts when undetermined.
netlify database migrations new -d "add users table"
netlify database migrations new -d "add users table" --scheme timestampnetlify database migrations apply
Applies pending migrations to the local development database. The CLI does not apply migrations to the local DB automatically when netlify dev starts — you run this command yourself when you're ready. Hosted databases (preview branches, production) are handled by the deploy.
netlify database migrations apply
netlify database migrations apply --to <name> # apply up to a specific migrationnetlify database migrations pull
Downloads migration files from a remote branch (defaults to production) and overwrites local files. Useful when local migration history has drifted from production — for example, after another contributor shipped a migration you don't have locally.
netlify database migrations pull # from production
netlify database migrations pull --branch staging # from a specific branch
netlify database migrations pull --branch # from your current local git branch
netlify database migrations pull --force # skip the overwrite confirmationnetlify database migrations reset
Deletes local migration files that have not yet been applied to the target database. Applied migrations and their data are left alone — the command can't undo something already applied.
Typical use: you generated a migration, realized it was wrong, and want to start over. Run reset, update db/schema.ts, then npm run db:generate produces a fresh migration.
netlify database migrations reset # against local dev DB
netlify database migrations reset --branch <name> # against a remote branchnetlify database reset
Wipes the local development database — drops all schemas and tables. Only affects the local DB; never touches preview branches or production. Use this when you want to replay all migrations from scratch.
netlify database resetLegacy Netlify DB extension (deprecated)
This reference covers the older Netlify DB extension (the Beta product), which is distinct from the current Netlify Database GA product described in SKILL.md.
Do not install or recommend the extension for new projects. Use it only when an existing project is already set up on it, and in that case encourage the user to switch to Netlify Database.
How to recognize an extension-based project
These signals indicate the project is on the legacy extension, not the GA product:
@netlify/neoninpackage.jsondependenciesNETLIFY_DATABASE_URLreferenced in code or env files (note: different from the GANETLIFY_DB_URL)- The Neon extension is installed under Extensions in the Netlify UI
The legacy extension was historically set up via netlify db init on older versions of the CLI. That command is gone in the current CLI — netlify db init (the short alias for netlify database init) now sets up Netlify Database (the GA product), not the extension. If a project shows the signals above, it was created on an older CLI version, not by anything reachable today.
Keeping an extension project working
If you land in a project that uses the extension:
- Don't rip it out unless the user has explicitly asked to migrate. The extension database holds their data and their Neon account holds the claim.
- Don't mix packages. Don't install
@netlify/databasealongside@netlify/neonwithout a migration plan — you'll end up with two databases and two env vars. - Keep using
@netlify/neonandNETLIFY_DATABASE_URLfor reads and writes in that codebase. - Migrations still belong in a migrations directory (commonly
netlify/db/migrations/or./migrations/) and are typically applied viadrizzle-kit migrate.
Encourage migration
The extension is deprecated. New database creation through the extension is blocked, and the Netlify UI surfaces deprecation notices when a project is using it. When you're helping a user on an extension-based project, proactively tell them:
- Netlify Database (GA) is the supported product going forward
- It removes the claim step, the separate Neon account, and the
@netlify/neon/NETLIFY_DATABASE_URLindirection - Switching is manual but well-documented. See
references/migration-from-extension.mdfor the full step-by-step process (covers switching from any external Postgres provider, with extension-specific callouts).
If the user agrees to switch, walk through the steps in that reference. Do not attempt the switch unprompted — confirm with the user first, as the process involves a brief downtime window and an operator step to import data.
Do not confuse the two
Common hallucinations to avoid:
- Using
@netlify/databasewithNETLIFY_DATABASE_URL(wrong env var) - Using
@netlify/neonwithNETLIFY_DB_URL(wrong env var) - Telling a user to "claim" their Netlify Database into a Neon account — that step only existed in the extension flow and is not part of Netlify Database (GA)
- Recommending
netlify db initto a legacy-extension user expecting it to reinstall the extension — the current CLI'sdb initsets up the GA product, not the extension - Assuming
netlify db <command>still targets the extension. It's the short alias fornetlify database <command>and runs the GA product.
When in doubt, check package.json and the env vars actually set on the site before suggesting commands.
Local development
netlify dev runs Netlify Database locally against an embedded Postgres-compatible instance — no remote connection, and no risk of writing to production data. Data persists under .netlify/ in the project directory.
Add .netlify to .gitignore if it isn't already.
Running the app
netlify devThe database is available to functions, edge functions, framework server routes, and any code that calls getDatabase() or getConnectionString() — same API as production.
For Vite-based projects, install @netlify/vite-plugin so the dev server can connect to the local database without launching netlify dev as a wrapper.
Applying migrations locally
netlify dev does not apply migrations automatically — that's the deploy's job for hosted databases. Locally, you run them yourself:
netlify database migrations apply # apply all pending
netlify database migrations apply --to <name> # apply up to a specific migrationThis targets the local dev DB only. Generating migrations from a Drizzle schema doesn't connect to a database, so plain npx drizzle-kit generate works — no wrapper needed.
Do not run drizzle-kit migrate or drizzle-kit push against NETLIFY_DB_URL in any context — Netlify applies migrations to hosted databases (preview branches and production) automatically on deploy. See references/migrations.md.
Inspecting the local DB
netlify database status # applied/pending state
netlify database connect # interactive REPL
netlify database connect --query "SELECT * FROM items" # one-shot query
netlify database connect --json # connection details as JSONFor tools that need a bare connection string (psql, pgAdmin, DataGrip, TablePlus), pipe connect --json through jq:
psql "$(netlify database connect --json | jq -r .connection_string)"Resetting local data
Use netlify database reset to wipe all schemas and tables in the local dev DB. Re-run netlify database migrations apply to replay the migration history from scratch.
netlify database reset
netlify database migrations applyCommon issues
- "Environment has not been configured": install
@netlify/vite-pluginor run the app vianetlify dev. - Schema drift between local and preview: confirm every schema change has a matching migration file in
netlify/database/migrations/committed to the branch. If local migration history has drifted, runnetlify database migrations pullto sync from a remote branch, ornetlify database migrations resetto clear unapplied local files. - Data not persisting across restarts: confirm the
.netlify/directory exists and is writable. A stale lockfile inside it can also cause startup failures — remove it ifnetlify devwon't boot.
Switching to Netlify Database
Step-by-step process for switching a project from an external Postgres provider to Netlify Database (@netlify/database, NETLIFY_DB_URL). The steps are provider-agnostic — they apply whether the source is the deprecated Netlify DB extension (@netlify/neon), a standalone Neon account, Supabase, RDS, a self-managed instance, or any other hosted Postgres.
Terminology. This document uses "switch" for the provider change and "migration" exclusively for schema migration files. The two are distinct operations that happen to overlap during this process.
Brief data-loss window. This flow trades a small data-loss risk for a much simpler cutover: any writes to the source between the final export and the production deploy will not make it across. For most projects that's a few minutes. High-traffic apps should plan a maintenance window or a dual-write strategy outside the scope of this guide.
Prerequisites
- A linked Netlify project currently serving from an existing Postgres source
- Netlify CLI 26.0.0+ installed and authenticated
pg_dumpandpg_restoreavailable locally, with versions matching your source server
The shape of the switch
Three phases, each independently reversible. The source database keeps serving production traffic until the Phase 2 merge, so any rollback before that has zero user-visible impact.
1. Phase 1 — Provision the new database alongside the source. No code or traffic changes. 2. Phase 2 — Swap the code and rehearse on a preview deploy with real data. 3. Phase 3 — Cut over production with a fresh data move and a merge.
Phase 1 — Provision the new database
Goal: Netlify Database is online with the correct schema baseline. App still reads from the source.
Switching from the Netlify DB extension.@netlify/databaseand@netlify/neonuse different env vars (NETLIFY_DB_URLvsNETLIFY_DATABASE_URL) and don't conflict. Keep@netlify/neoninstalled and the extension configured throughout the switch — cleanup happens at the end.
On a new branch:
1. Run netlify database init to install @netlify/database and verify the database is reachable. Decline the sample data prompt — a separate baseline migration follows in the next step:
netlify database init2. Create the baseline migration:
netlify database migrations new -d baseline3. Populate the new migration.sql with a schema-only dump of the source. What matters is that running this migration against an empty database leaves it with the right shape:
pg_dump --schema-only --no-owner --no-privileges "$SOURCE_DATABASE_URL"If the project already has Drizzle migrations, point drizzle-kit at netlify/database/migrations/ and move them in instead of the schema dump. pg_dump 18+ emits \restrict / \unrestrict psql meta-commands that are not valid SQL — strip them: ... | grep -v -E '^\\(restrict|unrestrict)'.
Switching from the Netlify DB extension with Neon Auth? The source contains aneon_authschema with auth tables. Add--schema=publicto exclude them. If you're switching auth providers too, handle that separately.
4. Push the branch. Netlify detects @netlify/database, provisions a preview database branch, and applies the baseline migration. The preview goes live still serving from the source database — app code hasn't changed yet.
5. Confirm the baseline applied cleanly:
netlify database status --branch <preview-branch>6. Merge the branch. Netlify provisions the production database branch and applies the baseline migration there too. Production still serves from the source.
If the baseline fails on the preview, the deploy fails and production is unaffected. Iterate until a clean preview deploy confirms the schema is reproducible from nothing.
Phase 2 — Swap the code and rehearse on a preview
Goal: the new production code works against Netlify Database, proven on a preview deploy with real data.
On a new branch:
1. Update application code to read and write through @netlify/database. Wire Drizzle to the native adapter:
// db/index.ts
import { drizzle } from "drizzle-orm/netlify-db";
import * as schema from "./schema";
export const db = drizzle({ schema });Switching from the Netlify DB extension. Replaceimport { neon } from "@netlify/neon"and any direct calls toneon()with the Drizzle adapter above. TheNETLIFY_DATABASE_URLenv var from the legacy extension is no longer read.
Not using Drizzle? The same flow works with any Postgres-compatible driver — see the native-driver section in SKILL.md.2. Update Drizzle config to point at the GA migrations directory:
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
dialect: "postgresql",
schema: "./db/schema.ts",
out: "netlify/database/migrations",
});3. Remove old-provider packages and any scripts that ran drizzle-kit migrate against explicit staging/production URLs. The GA product auto-applies schema migrations on every deploy.
Switching from the Netlify DB extension. Remove@netlify/neon,@neondatabase/serverless, and@neondatabase/toolkit. Keep@neondatabase/neon-jsonly if the frontend uses it for Neon Auth and auth is not being switched in this pass.
4. Push the branch. Netlify creates a preview deploy with its own preview database branch, forked from the (currently empty) production Netlify Database.
5. Get the preview branch's connection string with credentials:
netlify database status --branch <preview-branch> --show-credentials6. Copy a snapshot of data from the source into the preview branch. Use --data-only because the schema is already in place via the baseline migration, and --no-acl because Netlify Database manages its own privileges:
pg_dump -Fc --data-only "$SOURCE_DATABASE_URL" | pg_restore --no-owner --no-acl --dbname="$PREVIEW_DATABASE_URL"7. Exercise the preview URL — click through reads and writes, validate the critical flows end-to-end. If something's off, iterate on the branch and push again. Each push gets a fresh preview branch, so the rehearsal can be repeated until the path is clean.
The rehearsal is the core of this flow. By the time the preview looks right, both the code swap and the data move have been proven against a real deployed environment. The production cutover is a re-run of a path that's already been validated.
Phase 3 — Cut over production
When the rehearsal is clean:
1. Get the production database connection string with credentials:
netlify database status --show-credentials2. Export data from the source and import into production Netlify Database:
pg_dump -Fc --data-only "$SOURCE_DATABASE_URL" | pg_restore --no-owner --no-acl --dbname="$PRODUCTION_DATABASE_URL"3. Merge the Phase 2 branch to trigger a production deploy. Once it completes, the app reads and writes through Netlify Database.
4. Confirm reads and writes against the new production database.
Pre-flight: filename ordering for migrated migration files
If existing Drizzle migration files are being moved into netlify/database/migrations/ rather than baselined from a schema dump, filename ordering matters. Netlify applies schema migrations lexicographically by filename. If the project ever changed its Drizzle prefix setting (e.g., unix → timestamp), the lex order can diverge from _journal.json's idx order:
- 10-digit unix prefixes (
1771681020_...) sort before 14-digit timestamp prefixes (20260214140526_...) alphabetically - But the unix files may have been generated after the timestamp files chronologically
If lex sort of netlify/database/migrations/* does not match idx order in _journal.json, rename the offending files to timestamp prefixes using the when values from _journal.json:
date -u -r <unix_seconds> +%Y%m%d%H%M%S
git mv netlify/database/migrations/<old>_<name>.sql netlify/database/migrations/<new>_<name>.sql
git mv netlify/database/migrations/meta/<old>_snapshot.json netlify/database/migrations/meta/<new>_snapshot.json
# Update the `tag` in _journal.json to matchAlso walk the snapshot chain (id / prevId in each meta/<tag>_snapshot.json) and patch any broken prevId.
Rolling back
- Before merging Phase 2 — abandon the Phase 2 branch. Phase 1 left an empty Netlify Database behind a baseline migration; that's harmless.
- After merging Phase 2 — revert the merge in the Netlify UI. The app redeploys with the previous code, which still reads from the source. Keep the source running and its credentials live until production has been stable on Netlify Database long enough to trust the switch.
Cleanup
Once production has been stable on Netlify Database long enough to trust the switch:
- Remove the source database client from dependencies and any source connection strings from Netlify environment variables
- Decommission the source database in its hosting provider
Switching from the Netlify DB extension. Runnpm uninstall @netlify/neon, remove the Neon extension from the site under Extensions in the Netlify UI, and drop any remainingNETLIFY_DATABASE_URLreferences from code and environment. Deploy once more to finalize the removal.
Operational notes for agents
- Don't commit production data to source control. Pipe
pg_dumpdirectly intopg_restorerather than writing dumps to disk, or stage them in a gitignored directory (tmp/). Even with secrets stripped, PII and operational artifacts don't belong in git. - Don't run `drizzle-kit migrate` against the production connection string during or after the switch. Schema is the deploy's job — running it manually is exactly the kind of out-of-band change the rest of this skill warns against.
- The data import is the one documented exception to the rule "never connect to the production database directly." See
references/migrations.mdfor the broader rule. Once the switch is complete, resume using DML migrations for all production data changes.
Migrations
Netlify Database uses a file-based migration system. Migrations live in netlify/database/migrations/ and are applied automatically by Netlify: on every deploy preview before the preview is published, and on production immediately before publish. A failing migration blocks the deploy.
Prefer Drizzle Kit for generating migrations. Manual SQL migration files are an edge case — only hand-write one when Drizzle Kit can't express the change (for example, a Postgres-specific DDL or a targeted DML operation).
Never apply migrations to a hosted database yourself
The platform applies migrations to every Netlify-hosted database (preview branches and production) automatically on deploy. You never run drizzle-kit migrate against NETLIFY_DB_URL from a preview or production context. For local, use netlify database migrations apply — it targets the local development database only.
drizzle-kit push is not used in this workflow at all — always generate a migration file and let the deploy apply it. And never run DDL through netlify database connect, psql, or any other direct connection: schema changes out-of-band cause drift between the migration history and the actual database.
Schema migration workflow
1. Edit db/schema.ts 2. npm run db:generate (runs drizzle-kit generate) — writes a new file into netlify/database/migrations/ 3. Review the generated SQL 4. npm run db:migrate (runs netlify database migrations apply) — applies to the local dev DB for testing 5. Commit schema changes and the migration file together 6. Push — Netlify applies the migration to the preview branch, then to production on publish
Recommended package.json scripts:
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "netlify database migrations apply"
}
}netlify database migrations apply always targets the local dev DB. Running drizzle-kit migrate directly (especially with NETLIFY_DB_URL pointing at a hosted branch) is the wrong path — that's the deploy's job.
File layout and naming
Migrations go in netlify/database/migrations/. Two layouts are supported and can be mixed within a project:
- Flat: one
.sqlfile per migration —20260417143022_create_items.sql - Subdirectory: a folder containing
migration.sql—20260417143022_create_items/migration.sql(this is whatnetlify database migrations newand Drizzle Kit's default layout produce)
Files apply lexicographically. Timestamp prefixes are the default for both drizzle-kit generate and netlify database migrations new, and they keep filenames unique when two pieces of work generate migrations in parallel — common on teams and for solo developers iterating across branches.
If a project is already established on sequential prefixes (0000_, 0001_, …), leave it alone — the CLI's migrations new auto-detects the scheme — but expect collisions when working in parallel and resolve them by reset + regenerate.
netlify/database/migrations/
20260417143022_create_items.sql
20260418091500_add_items_is_active/
migration.sqlIterating on a migration you haven't shipped yet
If you generated a migration and realize it needs to change, what you do depends on whether it's been applied anywhere.
- Already applied to any database (local dev DB, preview branch, or production) → treat as immutable. Roll forward with a new migration.
- Only on disk → don't edit the SQL or snapshot files by hand. Run
netlify database migrations resetto delete the unapplied files, updatedb/schema.ts, then re-runnpm run db:generate. Hand-editing desyncs Drizzle Kit's internal state and tends to produce broken migrations on the next generate.
netlify database migrations reset only removes files that have not yet been applied — it's safe, and it cannot undo an applied migration. Use netlify database status to see what's applied vs pending before deciding. Pass --branch <name> to either command to target a remote preview branch instead of the local dev DB.
Recovering from drift with migrations pull
When local migration history has drifted from a remote branch — typically because another contributor (or another agent run) shipped a migration you don't have — pull the canonical files down:
netlify database migrations pull # from production
netlify database migrations pull --branch stagingmigrations pull overwrites local migration files with the ones from the target branch, so commit any local-only work first. After pulling, run netlify database migrations apply to bring the local dev DB up to date.
Preview branching
Each deploy preview runs against its own isolated database branch, forked from production data. This means:
- Migrations run against the preview branch first — failures fail the preview, not production
- Schema and data changes in a preview do not affect production until the branch is merged and published
- Agents and developers can test destructive migrations (drops, renames, type changes) without risk to production data
Ad-hoc edits made inside a preview (for example, through the Netlify UI's data browser) stay on that branch. They do not propagate to production. Always express production changes as migrations committed to the branch.
Breaking changes — expand and contract
For anything that could break running code (renaming a column, dropping a column, changing a type), use the expand-and-contract pattern so preview and production can coexist during the transition:
1. Expand: add the new shape alongside the old (new column, new table, nullable default). Deploy. 2. Migrate: backfill data and update application code to read/write both shapes, or switch to the new shape. Deploy. 3. Contract: drop the old shape once nothing reads or writes to it. Deploy.
Never combine these steps into a single migration that renames or drops in one shot while application code still depends on the old shape — the preview may pass, and production will break at cutover.
Production data changes — write a DML migration
When the user asks for data changes that should land in production (seed data, backfills, CSV imports, one-off cleanups, fixing a bad row), do not connect to the production database directly and do not run the change ad-hoc in a preview. Instead, generate a SQL migration file in netlify/database/migrations/ containing the DML.
-- netlify/database/migrations/20260417143022_backfill_item_slugs.sql
UPDATE items
SET slug = lower(regexp_replace(title, '[^a-zA-Z0-9]+', '-', 'g'))
WHERE slug IS NULL;After creating the migration:
- Tell the user, in plain language, that you created a data migration and that merging the branch will apply it to production
- Suggest they verify the result in the deploy preview (which runs against a forked copy of production data) before merging
- For large or risky backfills, recommend wrapping in a transaction or batching
Never take a shortcut — running the change directly in the Netlify UI data browser on production, or against the production connection string from a local shell, bypasses the migration history and creates drift between what the repo says the schema/data are and what production actually has.
One exception: initial data seed when switching database providers. When switching from an external database (including the legacy extension) to Netlify Database, production data must be imported via a direct connection — committing a full data dump to git is not appropriate. This one-time import is documented in references/migration-from-extension.md. Once the switch is complete, resume using DML migrations for all production data changes.
If the request is ambiguous ("fix the broken row for user X"), ask the user to confirm they want a production-bound migration rather than a one-off preview edit. When an agent is the one asking for data changes on behalf of a user, the default should be to not create a data migration unless the user has explicitly asked for production to change.
Admin interfaces instead of repeated DML migrations
If the user keeps needing to load or edit data (for example, "add a new teacher every week"), a one-off data migration each time is the wrong answer. Build them an admin interface — a page or CLI that uses the normal Drizzle client — so they can manage data through the application rather than through migrations. Gate it behind Netlify Identity or another auth mechanism (see netlify-identity/SKILL.md).
Manual SQL migrations
If you need to write a SQL migration by hand (for example, creating an extension, adding a check constraint Drizzle Kit won't emit, or a targeted DML statement), scaffold the file via the CLI:
netlify database migrations new -d "enable pgvector extension"This creates netlify/database/migrations/<prefix>_<slug>/migration.sql using the existing project's numbering scheme (or prompts for one). Open it and write the SQL. The flat layout (<prefix>_<slug>.sql directly in the migrations directory) also works if you prefer to write the file by hand.
Keep the SQL idempotent where possible (CREATE ... IF NOT EXISTS, guarded UPDATEs) so re-running against a half-migrated state is safe.
After adding a manual file in a Drizzle project, run the schema generate step anyway so Drizzle Kit's snapshot stays in sync with the current state of the database.
Related skills
How it compares
Pick netlify-database over generic Postgres skills when the app deploys on Netlify and needs first-class preview-branch database forking.
FAQ
What package provisions Netlify Database?
netlify-database documents @netlify/database in the dependency tree, which triggers auto-provisioning at deploy time and exposes NETLIFY_DB_URL to functions, edge handlers, and builds without a separate Neon account.
Which Drizzle version works with Netlify Database?
netlify-database requires drizzle-orm@beta and drizzle-kit@beta because the drizzle-orm/netlify-db adapter is only on the beta release line; latest stable Drizzle lacks that import.
Can agents run migrations on production Netlify branches?
netlify-database forbids applying migrations to hosted production or preview databases; agents commit migration files locally and Netlify applies them automatically during deploy.