← NetlifyCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Netlify
Snapshot Oct 7, 2026 · 00:02 UTC · version 1.6.0
Collection source: downloaded plugin package.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "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.",
"included_files": [
{
"relative_path": "references/cli-commands.md",
"size_in_bytes": 4272
},
{
"relative_path": "references/legacy-extension.md",
"size_in_bytes": 3482
},
{
"relative_path": "references/local-dev.md",
"size_in_bytes": 2926
},
{
"relative_path": "references/migration-from-extension.md",
"size_in_bytes": 10410
},
{
"relative_path": "references/migrations.md",
"size_in_bytes": 9533
},
{
"relative_path": "references/operational-footguns.md",
"size_in_bytes": 4010
}
],
"name": "netlify-database",
"skill_md_contents": "---\nname: netlify-database\ndescription: 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.\n---\n\n# Netlify Database\n\nZero-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.\n\n## Modern client (reach for this)\n\n```ts\nimport { getDatabase } from \"@netlify/database\";\n\nconst db = getDatabase(); // auto-selects connection for the runtime\nconst userId = 42;\nconst users = await db.sql`SELECT * FROM users WHERE id = ${userId}`; // auto-parameterized\n```\n\nOwn driver / ORM instead:\n```ts\nimport { getConnectionString } from \"@netlify/database\";\nconst connectionString = getConnectionString(); // correct branch for this env\n```\n\n**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`.\n\n## Where things go\n\n| What | Location |\n|------|----------|\n| Migrations | `netlify/database/migrations/` (SQL files or subdirs with `migration.sql`) |\n| Query code | Functions (`netlify/functions/`), Edge Functions |\n| Drizzle schema | `db/schema.ts` (convention) |\n| Drizzle client | `db/index.ts` (convention) |\n| Connection string | `NETLIFY_DB_URL` env var, or `getConnectionString()` |\n\n## Querying\n\n`getDatabase(options?)` returns a client with `sql` and `pool`. `options.connectionString` overrides the auto-provisioned one; `options.debug` enables logging.\n\n```ts\nconst db = getDatabase();\nconst active = await db.sql`SELECT * FROM users WHERE active = ${true}`;\nawait db.sql`INSERT INTO users (name, email) VALUES (${\"Ada\"}, ${\"ada@example.com\"})`;\nawait db.sql`UPDATE users SET name = ${\"Ada Lovelace\"} WHERE id = ${1}`;\nawait db.sql`DELETE FROM users WHERE id = ${1}`;\n\n// Type the rows\ninterface User { id: number; name: string; email: string; }\nconst typed = await db.sql<User>`SELECT * FROM users`;\n\n// Stream\nfor await (const row of db.sql`SELECT * FROM users`.stream()) { /* ... */ }\nfor await (const chunk of db.sql`SELECT * FROM users`.chunked(100)) { /* ... */ }\n```\n\n`SQLTemplate` methods: `execute()` → `Promise<T[]>`, `stream()` → `AsyncGenerator<T>`, `chunked(n)` → `AsyncGenerator<T[]>`, `toSQL()` → raw SQL + params without executing.\n\n`sql` helpers:\n- `sql.identifier(value)` — safe table/column name. String, string[], or `{ schema, table, column, as }`.\n- `sql.values(rows)` — bulk-insert values list from a 2D array.\n- `sql.default` — the SQL `DEFAULT` keyword.\n- `sql.raw(value)` — **injects unparameterized SQL; bypasses injection protection. Only for trusted constants (e.g. `\"DESC\"`), never user input.**\n- `sql.unsafe(query, params?, { rowMode })` — raw query string with `$1` params; `rowMode` is `\"array\"` or `\"object\"`.\n\n### Transactions — use `pool`\n\n`db.pool` is a [`pg.Pool`](https://node-postgres.com/apis/pool). `BEGIN`/queries/`COMMIT` must run on the same connection:\n```ts\nconst client = await db.pool.connect();\ntry {\n await client.query(\"BEGIN\");\n await client.query(\"INSERT INTO users (name, email) VALUES ($1, $2)\", [\"Ada\", \"ada@example.com\"]);\n await client.query(\"INSERT INTO posts (author_id, title) VALUES ($1, $2)\", [1, \"First post\"]);\n await client.query(\"COMMIT\");\n} catch (e) {\n await client.query(\"ROLLBACK\");\n throw e;\n} finally {\n client.release();\n}\n```\n\nOwn drivers:\n```ts\nimport { getConnectionString } from \"@netlify/database\";\nimport pg from \"pg\";\nconst pool = new pg.Pool({ connectionString: getConnectionString() });\n\n// or the `postgres` driver via env var\nimport postgres from \"postgres\";\nconst sql = postgres(process.env.NETLIFY_DB_URL);\n```\n\n## Drizzle ORM\n\n**Install both packages from `@beta` — required.** `latest` lacks the `drizzle-orm/netlify-db` adapter and will fail.\n```bash\nnpm install @netlify/database drizzle-orm@beta\nnpm install -D drizzle-kit@beta\n```\n\n`drizzle.config.ts` — you **MUST** set `out` to the Netlify migrations directory or Netlify won't apply generated migrations:\n```ts title=\"drizzle.config.ts\"\nimport { defineConfig } from \"drizzle-kit\";\nexport default defineConfig({\n dialect: \"postgresql\",\n schema: \"./db/schema.ts\",\n out: \"netlify/database/migrations\", // NOT the default \"drizzle\"\n});\n```\n\n```ts title=\"db/schema.ts\"\nimport { pgTable, serial, text, timestamp } from \"drizzle-orm/pg-core\";\nexport const users = pgTable(\"users\", {\n id: serial().primaryKey(),\n name: text().notNull(),\n email: text().notNull().unique(),\n createdAt: timestamp().defaultNow(),\n});\n```\n\n```ts title=\"db/index.ts\"\nimport { drizzle } from \"drizzle-orm/netlify-db\"; // native adapter, auto-configured\nimport * as schema from \"./schema\";\nexport const db = drizzle({ schema });\n```\n\n```ts title=\"netlify/functions/api.ts\"\nimport { desc } from \"drizzle-orm\";\nimport type { Config, Context } from \"@netlify/functions\";\nimport { db } from \"../../db\";\nimport { users } from \"../../db/schema\";\n\nexport default async (req: Request, context: Context) => {\n if (req.method === \"GET\") {\n const allUsers = await db.select().from(users).orderBy(desc(users.createdAt));\n return Response.json(allUsers);\n }\n if (req.method === \"POST\") {\n const { name, email } = await req.json();\n const [user] = await db.insert(users).values({ name, email }).returning();\n return Response.json(user, { status: 201 });\n }\n return new Response(\"Method not allowed\", { status: 405 });\n};\n\nexport const config: Config = { path: \"/api/users\" };\n```\n\nGenerate migrations after editing the schema: `npx drizzle-kit generate`.\n\n**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.\n\n## Migrations\n\nFiles live in `netlify/database/migrations/`. Two formats:\n```text\nnetlify/database/migrations/20260301143000_create_users.sql # single SQL file\nnetlify/database/migrations/20260318091500_add_posts/migration.sql # subdir form\n```\n\nNaming: `<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.\n\n```sql title=\"netlify/database/migrations/20260425103000_create_comments.sql\"\nCREATE TABLE comments (\n id SERIAL PRIMARY KEY,\n post_id INTEGER NOT NULL REFERENCES posts(id),\n author_id INTEGER NOT NULL REFERENCES users(id),\n body TEXT NOT NULL,\n created_at TIMESTAMP DEFAULT NOW()\n);\n```\n\n**When applied:**\n- Production deploy: applied immediately before publish; a failure blocks publish. With auto-publish off, Netlify waits for manual publish before applying.\n- Deploy preview: applied on every deploy before it goes live; a failure fails the deploy.\n- Local: **not** automatic — run `netlify database migrations apply` yourself.\n\n**Migration footguns (all detected as drift / rejected):**\n- **Never edit an applied migration** — checksum drift: `migration \"<name>\" has been modified after being applied`. Write a new corrective migration.\n- **Never remove an applied migration** — `... has been removed after being applied`. Restore it.\n- **Out-of-order:** a prefix ≤ the highest applied version is rejected. Timestamps avoid this.\n- Prefer backwards-compatible migrations. Breaking changes (rename/drop column) → expand-and-contract across multiple deploys. New table / nullable column → single migration is fine.\n\nBring-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.\n\nSee `references/migrations.md`.\n\n## Local development\n\nLocal 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.\n\nStart it — either path, state is interchangeable:\n```bash\nnetlify dev # CLI starts + tears down the local DB\n```\nOr the Vite plugin:\n```ts title=\"vite.config.ts\"\nimport { defineConfig } from \"vite\";\nimport netlify from \"@netlify/vite-plugin\";\nexport default defineConfig({ plugins: [netlify()] });\n```\n\nCommon commands (while local DB is running):\n```bash\nnetlify database migrations apply # apply pending locally\nnetlify database migrations new -d \"add users table\" # scaffold new migration\nnetlify database migrations pull # overwrite local migrations from remote\nnetlify database status # enabled? installed? applied/pending migrations\nnetlify database connect # interactive SQL REPL\nnetlify database connect --query \"SELECT * FROM users LIMIT 10\"\nnetlify database reset # drop all schemas/tables — LOCAL ONLY\nnetlify database migrations reset # delete unapplied local migration files\n```\n\nExternal tools (works while `netlify dev` runs):\n```bash\npsql \"$(netlify database connect --json | jq -r .connection_string)\"\n```\n\nSee `references/local-dev.md`.\n\n## Setup\n\nNew project: describe your app to Agent Runners at https://app.netlify.com/start, or `netlify create \"<description>\"` locally.\n\nExisting project:\n```bash\nnetlify database init # installs @netlify/database, picks Drizzle or raw SQL, scaffolds a migration\nnetlify database init --yes # non-interactive (CI / agents)\nnetlify dev\n```\nManual: `npm install @netlify/database`, write a migration under `netlify/database/migrations/`, write a function, `netlify dev`, deploy.\n\n**If `@netlify/database` is NOT installed, Netlify will NOT auto-provision a database** — you'd have to create one manually from the UI **Data & Storage** > **Database** menu. Install the package.\n\n## CLI reference (`netlify database`)\n\nPrereqs: Node ≥ 20.12.2, Netlify CLI ≥ 26.0.0 (`npm install -g netlify-cli`). All commands support `--json`.\n\n| Command | Purpose | Key flags |\n|---------|---------|-----------|\n| `init` | Set up DB in project | `-y, --yes` |\n| `status` | State: enabled, installed, connection string, applied/pending migrations | `-b, --branch`, `--show-credentials` |\n| `connect` | SQL REPL, or `--query` one-shot | `-q, --query`, `--json` |\n| `migrations apply` | Apply pending to local DB | `--to <name>` |\n| `migrations new` | Scaffold a migration | `-d, --description`, `-s, --scheme sequential\\|timestamp` |\n| `migrations pull` | Overwrite local files from a branch | `-b, --branch`, `--force` |\n| `migrations reset` | Delete unapplied local migration files | `-b, --branch` |\n| `reset` | Drop all data/tables — **local only** | — |\n\nSee `references/cli-commands.md`.\n\n## REST API\n\nScoped to a site, rooted at `https://api.netlify.com/api/v1`, OAuth 2. Full reference: https://open-api.netlify.com.\n\n| Method + path | Purpose |\n|---------------|---------|\n| `POST /sites/{site_id}/database` | Create DB (returns existing conn string if present); `region` optional |\n| `GET /sites/{site_id}/database` | Get connection string |\n| `POST /sites/{site_id}/database/branch` | Create branch; body `deploy_id` (req), `parent_branch_id` (opt, defaults to production) |\n| `GET /sites/{site_id}/database/branch/{deploy_id}` | Get branch conn string (404 if none) |\n| `DELETE /sites/{site_id}/database/branch/{deploy_id}` | Delete a deploy's branch |\n| `POST /sites/{site_id}/database/snapshot` | Snapshot a branch (defaults production) |\n| `GET /sites/{site_id}/database/snapshots` | List snapshots |\n| `DELETE /sites/{site_id}/database/snapshot/{snapshot_id}` | Delete a snapshot |\n| `POST /sites/{site_id}/database/snapshot/{snapshot_id}/restore` | Restore snapshot to a branch (defaults production) |\n\n**Branch delete and snapshot restore are destructive and require explicit user confirmation first.** Snapshot restore is not a routine production-rollback lever.\n\n## Testing\n\nBare Postgres for unit/integration tests (no functions):\n```ts title=\"db.test.ts\"\nimport { NetlifyDB } from \"@netlify/database-dev\"; // npm i -D @netlify/database-dev\nimport { Client } from \"pg\";\nimport { afterAll, beforeAll, expect, test } from \"vitest\";\n\nlet db: NetlifyDB, connectionString: string;\nbeforeAll(async () => {\n db = new NetlifyDB();\n connectionString = await db.start();\n await db.applyMigrations(\"./netlify/database/migrations\");\n});\nafterAll(async () => { await db.stop(); });\n\ntest(\"inserts and reads a user\", async () => {\n const client = new Client({ connectionString });\n await client.connect();\n await client.query(\"INSERT INTO users (name) VALUES ($1)\", [\"Ada\"]);\n const { rows } = await client.query(\"SELECT name FROM users\");\n expect(rows).toEqual([{ name: \"Ada\" }]);\n await client.end();\n});\n```\n`NetlifyDB(options?)`: `directory` (persist to disk; omit = in-memory), `port` (default random), `logger`.\n\nFull Netlify environment (functions/edge functions read `NETLIFY_DB_URL` as in production):\n```ts\nimport { NetlifyDev } from \"@netlify/dev\"; // npm i -D @netlify/dev\nconst netlifyDev = new NetlifyDev({ projectRoot: \"./fixtures/my-project\" });\nawait netlifyDev.start(); // sets NETLIFY_DB_URL in the runtime\n// ...tests...\nawait netlifyDev.stop();\n```\n\n## Database branches (deploy-time)\n\nProduction 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.\n\n**Preview branches can contain production data, including PII — and preview deploy links are public. Warn the user before sharing a preview link.**\n\n## Runtime gotchas\n\n- **`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.\n ```ts\n const db = getDatabase({ connectionString: \"postgres://...\" });\n ```\n Lambda compatibility mode is the one primitive where you must pass `connectionString` yourself.\n- **`database feature not available for this account`** — requires a Credit-based plan.\n- **`compute customization requires a Pro or higher plan`** — auto-scale / sleep settings need Pro+; Free/Personal use defaults.\n- **`branch limit reached: maximum <N> branches...`** — each active deploy preview consumes a branch; delete unneeded branches or upgrade.\n- **`database not found`** — no DB provisioned; run `netlify database init`.\n- **`cannot reset the production branch`** — reset is non-production only.\n\n## Constraints\n\n- **Plan:** Netlify Database is available on Credit-based plans only; active DBs consume credits for compute and bandwidth. Storage is free until July 1, 2026.\n- **Permissions:** only a Team Owner can delete a database; only Team Owners and Developers can view connection strings (`Access Denied` = insufficient role).\n- **Secrets:** connection strings contain username + password. Never commit them; store in a secret manager / env var provider.\n\n## Switch an existing Postgres project to Netlify Database\n\nThree 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.\n\nPhase 2/3 code swap (Drizzle):\n```ts title=\"db/index.ts\"\nimport { drizzle } from \"drizzle-orm/netlify-db\";\nimport * as schema from \"./schema\";\nexport const db = drizzle({ schema });\n```\n\nFull step-by-step (dump flags, rollback, cleanup): `references/migration-from-extension.md` and `references/legacy-extension.md`.\n\n<!-- Gaps: plan-tier naming (Credit-based vs Free/Personal/Pro) not reconciled in source; exact plan limits, permission tables, and snapshot UI flows live on pages outside this grouping. -->\n\n<!-- system: agent-context/database/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->\n# Netlify house rules (database)\n\nThese are org conventions, not docs facts — merged into the rendered skill by\nctx-gen and never generated. Owned by the skills maintainer.\n\n1. Production data changes are expressed as DML migrations — agents never\n edit rows directly (UI row editing exists for humans; it is not an agent\n surface).\n2. Preview branches can contain production data, including PII — and preview\n deploy links are public. Warn before sharing.\n3. Use only documented surfaces: no raw psql against internal endpoints, no\n `netlify api` scraping, no reading tokens from local CLI config files.\n4. Deep guides live in this skill: `references/operational-footguns.md`,\n `references/migrations.md`, `references/local-dev.md`,\n `references/cli-commands.md`, `references/migration-from-extension.md`,\n `references/legacy-extension.md`.\n5. Schema changes reach hosted databases only as committed migration files\n applied by the deploy. Never run `drizzle-kit push` in any form against a\n Netlify-hosted database, never run `drizzle-kit migrate` against\n `NETLIFY_DB_URL`, and never apply DDL via `netlify database connect` or\n any direct connection.\n6. When a `netlify` command or a deploy fails, surface the exact error, the\n deploy log URL, and the affected site/branch to the user and stop — do\n not invent recovery commands or escalate to lower-level tools.\n7. First-deploy `401 Access Denied` on `createSiteDatabase`: if it happened\n on a `--prod`-first deploy, retry preview-first (`netlify deploy`, no\n `--prod`); if a preview also fails, report and stop. Never curl\n `api.netlify.com`, run `netlify api createSiteDatabase`, or pull tokens\n from local CLI config to work around it.\n8. A request to change existing data is ambiguous between production and the\n preview branch — if the prompt didn't say, ask. When acting on someone's\n behalf, default to not touching production.\n9. Destructive database operations — REST branch delete, snapshot restore,\n any reset — require explicit user confirmation first. The body must not\n present snapshot restore as a routine production-rollback lever.\n10. Pin: `drizzle-orm` and `drizzle-kit` must be installed from `@beta` —\n `latest` lacks the `drizzle-orm/netlify-db` adapter and will fail. The\n body may not soften this to a recommendation.\n"
}SHA-256 of public snapshot: 6af7195042615d6aa2d864d84975cb7f971625141ec8747f1c9d4797555480aa