npx skills add ...
npx skills add netlify/context-and-tools --skill netlify-database
Zero-config Postgres for Netlify apps via @netlify/database — querying data from Functions/Edge Functions, writing schema migrations, setting up Drizzle ORM, local dev with netlify dev, database branches for deploy previews, and migrating an existing Postgres project onto Netlify. Use when adding a database, building a contact form or CRUD API, writing SQL migrations, wiring up Drizzle, running netlify database commands, testing with a local Postgres, or switching from Neon/Supabase/RDS to Netlify Database.
npx skills add netlify/context-and-tools --skill netlify-database
Zero-config managed Postgres. Install @netlify/database, write migrations under netlify/database/migrations/, deploy — Netlify provisions the DB and applies migrations automatically. Queryable from Functions, Edge Functions, Builds, and Agent Runners.
Own driver / ORM instead:
Legacy — do NOT use for new code: import { neon } from "@netlify/neon". Superseded by @netlify/database. Replace neon() calls with the Drizzle netlify-db adapter or a Postgres driver via getConnectionString(). The legacy env var NETLIFY_DATABASE_URL is replaced by NETLIFY_DB_URL.
| What | Location |
|---|---|
| Migrations | netlify/database/migrations/ (SQL files or subdirs with migration.sql) |
| Query code | Functions (netlify/functions/), Edge Functions |
| Drizzle schema | db/schema.ts (convention) |
| Drizzle client | db/index.ts (convention) |
| Connection string | NETLIFY_DB_URL env var, or getConnectionString() |
getDatabase(options?) returns a client with sql and pool. options.connectionString overrides the auto-provisioned one; options.debug enables logging.
SQLTemplate methods: execute() → Promise<T[]>, stream() → AsyncGenerator<T>, chunked(n) → AsyncGenerator<T[]>, toSQL() → raw SQL + params without executing.
sql helpers:
sql.identifier(value) — safe table/column name. String, string[], or { schema, table, column, as }.sql.values(rows) — bulk-insert values list from a 2D array.sql.default — the SQL DEFAULT keyword.sql.raw(value) — injects unparameterized SQL; bypasses injection protection. Only for trusted constants (e.g. "DESC"), never user input.sql.unsafe(query, params?, { rowMode }) — raw query string with $1 params; rowMode is "array" or "object".pooldb.pool is a pg.Pool. BEGIN/queries/COMMIT must run on the same connection:
Own drivers:
Install both packages from @beta — required. latest lacks the drizzle-orm/netlify-db adapter and will fail.
drizzle.config.ts — you MUST set out to the Netlify migrations directory or Netlify won't apply generated migrations:
Generate migrations after editing the schema: npx drizzle-kit generate.
Never run drizzle-kit push against a Netlify-hosted database, and never run drizzle-kit migrate against NETLIFY_DB_URL. Schema reaches hosted DBs only as committed migration files applied by the deploy. generate writes files; the deploy applies them.
Files live in netlify/database/migrations/. Two formats:
Naming: <number>_<slug> — number is digits (timestamp or 0001…) defining order; slug is lowercase letters/numbers/hyphens/underscores. Sorted lexicographically, applied in order. Use timestamp prefixes (netlify database migrations new handles this) to avoid out-of-order rejection.
When applied:
netlify database migrations apply yourself.Migration footguns (all detected as drift / rejected):
migration "<name>" has been modified after being applied. Write a new corrective migration.... has been removed after being applied. Restore it.Bring-your-own migration system: pick a directory other than netlify/database/migrations to avoid automatic detection, and you own applying to preview branches and production.
See references/migrations.md.
Local is one database that all code targets — branches are a deploy-time concept and don't exist locally. It's a real Postgres-compatible engine mirroring production, but single-process (not for load testing); auto-scale/sleep settings don't apply.
Start it — either path, state is interchangeable:
Or the Vite plugin:
Common commands (while local DB is running):
External tools (works while netlify dev runs):
See references/local-dev.md.
New project: describe your app to Agent Runners at https://app.netlify.com/start, or netlify create "<description>" locally.
Existing project:
Manual: npm install @netlify/database, write a migration under netlify/database/migrations/, write a function, netlify dev, deploy.
If @netlify/database is NOT installed, Netlify will NOT auto-provision a database — you'd have to create one manually from the UI Database menu. Install the package.
netlify database)Prereqs: Node ≥ 20.12.2, Netlify CLI ≥ 26.0.0 (npm install -g netlify-cli). All commands support --json.
| Command | Purpose | Key flags |
|---|---|---|
init | Set up DB in project | -y, --yes |
status | State: enabled, installed, connection string, applied/pending migrations | -b, --branch, --show-credentials |
connect | SQL REPL, or --query one-shot | -q, --query, --json |
migrations apply | Apply pending to local DB | --to <name> |
migrations new | Scaffold a migration | -d, --description, -s, --scheme sequential|timestamp |
migrations pull | Overwrite local files from a branch | -b, --branch, --force |
migrations reset | Delete unapplied local migration files | -b, --branch |
reset | Drop all data/tables — local only | — |
See references/cli-commands.md.
Scoped to a site, rooted at https://api.netlify.com/api/v1, OAuth 2. Full reference: https://open-api.netlify.com.
| Method + path | Purpose |
|---|---|
POST /sites/{site_id}/database | Create DB (returns existing conn string if present); region optional |
GET /sites/{site_id}/database | Get connection string |
POST /sites/{site_id}/database/branch | Create branch; body deploy_id (req), parent_branch_id (opt, defaults to production) |
GET /sites/{site_id}/database/branch/{deploy_id} | Get branch conn string (404 if none) |
DELETE /sites/{site_id}/database/branch/{deploy_id} | Delete a deploy's branch |
POST /sites/{site_id}/database/snapshot | Snapshot a branch (defaults production) |
GET /sites/{site_id}/database/snapshots | List snapshots |
DELETE /sites/{site_id}/database/snapshot/{snapshot_id} | Delete a snapshot |
POST /sites/{site_id}/database/snapshot/{snapshot_id}/restore | Restore snapshot to a branch (defaults production) |
Branch delete and snapshot restore are destructive and require explicit user confirmation first. Snapshot restore is not a routine production-rollback lever.
Bare Postgres for unit/integration tests (no functions):
NetlifyDB(options?): directory (persist to disk; omit = in-memory), port (default random), logger.
Full Netlify environment (functions/edge functions read NETLIFY_DB_URL as in production):
Production deploys are the only deploys that touch the production database. Each deploy preview gets its own branch, seeded with a copy of production data at preview-creation time; schema/data changes there never affect production. Wired up automatically, no code changes.
Preview branches can contain production data, including PII — and preview deploy links are public. Warn the user before sharing a preview link.
Environment not configured (getDatabase() can't resolve a connection string): running outside Netlify, on Functions in Lambda compatibility mode, or an outdated CLI. Fix: pass connectionString explicitly.
connectionString yourself.database feature not available for this account — requires a Credit-based plan.compute customization requires a Pro or higher plan — auto-scale / sleep settings need Pro+; Free/Personal use defaults.branch limit reached: maximum <N> branches... — each active deploy preview consumes a branch; delete unneeded branches or upgrade.database not found — no DB provisioned; run netlify database init.cannot reset the production branch — reset is non-production only.Access Denied = insufficient role).Three phases: provision (baseline schema on a branch), rehearse (swap code, copy data into a preview branch, validate), cut over (import data into production, merge). Works from any Postgres source (Neon, Supabase, RDS, self-managed, legacy @netlify/neon). Uses pg_dump/pg_restore (versions matching the source). There is a brief data-loss window — writes to the source between final export and production deploy don't cross over.
Phase 2/3 code swap (Drizzle):
Full step-by-step (dump flags, rollback, cleanup): references/migration-from-extension.md and references/legacy-extension.md.
These are org conventions, not docs facts — merged into the rendered skill by ctx-gen and never generated. Owned by the skills maintainer.
netlify api scraping, no reading tokens from local CLI config files.references/operational-footguns.md,
references/migrations.md, references/local-dev.md,
references/cli-commands.md, references/migration-from-extension.md,
references/legacy-extension.md.drizzle-kit push in any form against a
Netlify-hosted database, never run drizzle-kit migrate against
NETLIFY_DB_URL, and never apply DDL via netlify database connect or
any direct connection.netlify command or a deploy fails, surface the exact error, the
deploy log URL, and the affected site/branch to the user and stop — do
not invent recovery commands or escalate to lower-level tools.401 Access Denied on createSiteDatabase: if it happened
on a --prod-first deploy, retry preview-first (netlify deploy, no
--prod); if a preview also fails, report and stop. Never curl
api.netlify.com, run netlify api createSiteDatabase, or pull tokens
from local CLI config to work around it.drizzle-orm and drizzle-kit must be installed from @beta —
latest lacks the drizzle-orm/netlify-db adapter and will fail. The
body may not soften this to a recommendation.import { getConnectionString } from "@netlify/database";
const connectionString = getConnectionString(); // correct branch for this envconst db = getDatabase();
const active = await db.sql`SELECT * FROM users WHERE active = ${true}`;
await db.sql`INSERT INTO users (name, email) VALUES (${"Ada"}, ${"ada@example.com"})`;
await db.sql`UPDATE users SET name = ${"Ada Lovelace"} WHERE id = ${1}`;
await db.sql`DELETE FROM users WHERE id = ${1}`;
// Type the rows
interface User { id: number; name: string; email: string; }
const typed = await db.sql<User>`SELECT * FROM users`;
// Stream
for await (const row of db.sql`SELECT * FROM users`.stream()) { /* ... */ }
for await (const chunk of db.sql`SELECT * FROM users`.chunked(100)) { /* ... */ }const client = await db.pool.connect();
try {
await client.query("BEGIN");
await client.query("INSERT INTO users (name, email) VALUES ($1, $2)", ["Ada", "ada@example.com"]);
await client.query("INSERT INTO posts (author_id, title) VALUES ($1, $2)", [1, "First post"]);
await client.query("COMMIT");
} catch (e) {
await client.query("ROLLBACK");
throw e;
} finally {
client.release();
}import { getConnectionString } from "@netlify/database";
import pg from "pg";
const pool = new pg.Pool({ connectionString: getConnectionString() });
// or the `postgres` driver via env var
import postgres from "postgres";
const sql = postgres(process.env.NETLIFY_DB_URL);npm install @netlify/database drizzle-orm@beta
npm install -D drizzle-kit@beta