---
name: netlify-database
description: Use Netlify Database to store application data — records, collections, lists, user data, or any structured information. Provides a fully managed Postgres database. Use for CRUD operations, queryable data, or anything you would naturally model as rows in a table. Use Blobs only for files, binary assets, or cached responses.
---

# Netlify Database

Netlify Database is a fully managed Postgres database built into the Netlify platform. It handles provisioning, migrations,
and database branching automatically. There is no setup beyond installing the `@netlify/database` package.

Prefer Netlify Database over Netlify Blobs for any application data — records, collections, lists, user-generated content,
or anything you would store as rows. Use Blobs only for files, binary assets (images, video), or cached responses.

## CRITICAL: Use the `@beta` dist-tag for Drizzle

The Netlify Database adapter for Drizzle ORM only exists on the `beta` release line of `drizzle-orm`. You MUST install
both `drizzle-orm` and `drizzle-kit` from the `@beta` dist-tag — the default `latest` versions do not include the
Netlify Database adapter and will fail.

This affects:
- `npm install` commands: always install `drizzle-orm@beta` and `drizzle-kit@beta` (never `drizzle-orm` or
  `drizzle-kit` without the tag).
- Import paths: use `drizzle-orm`, `drizzle-orm/pg-core`, and `drizzle-orm/netlify-db` as normal. No package
  name rewriting is needed — the `@beta` tag only affects the installed version, not the imports.

If you forget the `@beta` tag, `drizzle-kit generate` will error with a message about being outdated, or the
`drizzle-orm/netlify-db` import will fail to resolve.

## CRITICAL: Database schema changes always require migrations

Any code change that assumes a different database schema (adding tables, adding columns, renaming fields, changing types,
adding indexes, etc.) will only work if a corresponding migration exists in `netlify/database/migrations/`. Without a
migration, the database schema will not change and the application will break at runtime.

This applies regardless of whether you are using Drizzle ORM or the native `@netlify/database` driver:
- **With Drizzle ORM**: Update the schema definition, then run `npx drizzle-kit generate`
- **With native driver**: Write a new SQL migration file in `netlify/database/migrations/`

Never consider a schema change complete without a corresponding migration file.

## Setup with Drizzle ORM (recommended)

This is the recommended approach. It gives you type-safe schema definitions, a query builder, and automatic migration
generation.

### Step 1: Install packages

```bash
npm install @netlify/database drizzle-orm@beta
npm install -D drizzle-kit@beta
```

### Step 2: Create the schema file

Create `db/schema.ts` at the project root. Define all your tables here using Drizzle's schema builder.

```typescript
// db/schema.ts
import { pgTable, serial, text, timestamp, integer } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: serial().primaryKey(),
  name: text().notNull(),
  email: text().notNull().unique(),
  createdAt: timestamp("created_at").defaultNow(),
});
```

When adding tables with foreign keys:

```typescript
export const posts = pgTable("posts", {
  id: serial().primaryKey(),
  title: text().notNull(),
  content: text().notNull().default(""),
  authorId: integer("author_id").notNull().references(() => users.id),
  createdAt: timestamp("created_at").defaultNow(),
});
```

IMPORTANT: Use snake_case strings for column names (e.g. `"created_at"`, `"author_id"`) to match Postgres conventions.
The Drizzle column variable names can be camelCase.

### Step 3: Create the database client

Create `db/index.ts`. This file initializes the Drizzle client with the native Netlify Database adapter.

```typescript
// db/index.ts
import { drizzle } from "drizzle-orm/netlify-db";
import * as schema from "./schema.js";

export const db = drizzle({ schema });
```

The connection is configured automatically — no connection string needed.

### Step 4: Configure Drizzle Kit

Create `drizzle.config.ts` at the project root. The `out` property MUST be set to `netlify/database/migrations` for
Netlify to automatically apply migrations.

```typescript
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  dialect: "postgresql",
  schema: "./db/schema.ts",
  out: "netlify/database/migrations",
});
```

### Step 5: Generate migrations

Every time you change the schema, run `drizzle-kit generate`:

```bash
npx drizzle-kit generate
```

This creates SQL migration files in `netlify/database/migrations/`.

### Step 6: Use the database in functions

```typescript
// netlify/functions/api.ts
import type { Config } from "@netlify/functions";
import { db } from "../../db/index.js";
import { users } from "../../db/schema.js";

export default async (req: Request) => {
  if (req.method === "GET") {
    const allUsers = await db.select().from(users);
    return Response.json(allUsers);
  }

  if (req.method === "POST") {
    const { name, email } = await req.json();
    const [user] = await db.insert(users).values({ name, email }).returning();
    return Response.json(user, { status: 201 });
  }

  return new Response("Method not allowed", { status: 405 });
};

export const config: Config = {
  path: "/api/users",
};
```

### Drizzle ORM query reference

```typescript
import { eq, desc, and, or, like, sql } from "drizzle-orm";
import { db } from "./db/index.js";
import { users, posts } from "./db/schema.js";

// Select all
const allUsers = await db.select().from(users);

// Select with condition
const user = await db.select().from(users).where(eq(users.id, 1));

// Select with ordering
const sorted = await db.select().from(users).orderBy(desc(users.createdAt));

// Select with limit
const first10 = await db.select().from(users).limit(10);

// Select specific columns
const names = await db.select({ name: users.name }).from(users);

// Insert one row
const [inserted] = await db.insert(users).values({ name: "Ada", email: "ada@example.com" }).returning();

// Insert multiple rows
await db.insert(users).values([
  { name: "Ada", email: "ada@example.com" },
  { name: "Bob", email: "bob@example.com" },
]);

// Update
await db.update(users).set({ name: "Ada Lovelace" }).where(eq(users.id, 1));

// Delete
await db.delete(users).where(eq(users.id, 1));

// Join
const postsWithAuthors = await db
  .select()
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id));
```

## Setup with native driver (alternative)

Use this when you prefer raw SQL or don't want Drizzle ORM.

### Install

```bash
npm install @netlify/database
```

### Query with `db.sql`

```typescript
import { getDatabase } from "@netlify/database";

const db = getDatabase();

// Select
const users = await db.sql`SELECT * FROM users`;

// Select with parameters (automatically parameterized)
const user = await db.sql`SELECT * FROM users WHERE id = ${userId}`;

// Insert with RETURNING
const [newUser] = await db.sql`
  INSERT INTO users (name, email)
  VALUES (${"Ada"}, ${"ada@example.com"})
  RETURNING *
`;

// Update
await db.sql`UPDATE users SET name = ${"Ada Lovelace"} WHERE id = ${1}`;

// Delete
await db.sql`DELETE FROM users WHERE id = ${1}`;

// Bulk insert
const data = db.sql.values([
  ["Ada", "ada@example.com"],
  ["Bob", "bob@example.com"],
]);
await db.sql`INSERT INTO users (name, email) VALUES ${data}`;
```

### Transactions with `db.pool`

```typescript
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)", ["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();
}
```

### Write migrations manually

When using the native driver, write SQL migration files by hand in `netlify/database/migrations/`.

Each migration is a SQL file named `<number>_<slug>.sql`:

```
netlify/database/migrations/
├── 20260417143022_create-users.sql
├── 20260418091500_add-posts.sql
└── 20260419112330_create-comments.sql
```

Example migration:

```sql
-- netlify/database/migrations/20260417143022_create-users.sql
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  email TEXT UNIQUE NOT NULL,
  created_at TIMESTAMP DEFAULT NOW()
);
```

Rules for migration names:
- `number` is any sequence of digits (e.g. `001`, `0001`, or a Unix timestamp)
- `slug` contains only lowercase letters, numbers, hyphens, and underscores
- Migrations are sorted lexicographically and applied in order
- NEVER modify a migration file once it has been applied to any database (local, preview, or production).
  Unapplied migration files can be freely deleted and regenerated; see "Iterating on migrations within a
  session" below.

## Migrations: how they work

IMPORTANT: You must NEVER apply migrations yourself. Your job is only to CREATE migration files — either by running
`npx drizzle-kit generate` (when using Drizzle ORM) or by writing SQL files in `netlify/database/migrations/` (when using
the native driver). The Netlify platform applies migrations automatically at the right point in the deploy lifecycle.
Do NOT run `drizzle-kit migrate`, `drizzle-kit push`, or execute migration SQL directly against the database.

Netlify applies migrations automatically during deploys:

- **Production deploys:** Applied immediately before the deploy is published. A failure blocks publishing.
- **Deploy previews:** Applied on every new deploy, immediately before it becomes available. A failure fails the deploy.

Migrations MUST be in `netlify/database/migrations/` to be applied automatically.

Two formats are supported:
1. SQL files directly in the directory (e.g. `20260417143022_create-users.sql`)
2. Subdirectories containing a `migration.sql` file (e.g. `20260417143022_create-users/migration.sql`)

## Database branches

- **Production deploys** access the main database only.
- **Deploy previews** get their own isolated database branch with a copy of production data.
- Changes in a deploy preview's branch never affect the production database.
- You do NOT need to do anything to enable branching — it is automatic.
- A preview branch persists across multiple runs in the same session. Migrations you applied in an earlier run
  remain applied when a later run starts, and any test data you inserted persists too. This matters when iterating
  on migrations — see "Iterating on migrations within a session" below.

## Inspecting migration state

The current database state — which migrations are already applied to your preview branch, and which migration
files exist on disk but haven't been applied yet — is included in your context at the start of the run, under
the "Current Netlify Database state" heading. Read that list before planning any migration work. You do not
need to run `netlify db status` at the start of a task to get this information.

If you need to re-check state mid-session — typically after generating a new migration, applying migrations, or
running `netlify db migrations reset` — run:

```bash
netlify db status
```

The command targets your preview database branch automatically (`NETLIFY_DB_BRANCH` is pre-set).

It reports:
- **Applied**: migrations that have been applied to the branch
- **Pending**: migration files that exist locally but have not been applied
- **Missing on disk**: migrations applied on the server but not present locally (drift — this is a serious
  state; never intentionally create it)
- **Out of order**: pending migrations whose prefix sorts before an already-applied migration. The platform
  rejects these, so fix via the "Rule of thumb" below (reset + regenerate). If `drizzle-kit generate` then
  reports no changes, the out-of-order files were redundant and deleting them was the complete fix.

## Iterating on migrations within a session

A single agent session can run multiple times, and all runs share one preview database branch. When you realize
a migration from an earlier run needs to change, the correct action depends on whether the migration has been
applied to the database yet.

### Rule of thumb

- If a migration has been **applied** to any database (local dev DB, preview branch, or production), treat it
  as immutable. Roll forward with a new migration that applies the correction. This is always safe.
- If a migration is **not yet applied** anywhere (it's only on disk), don't edit it in place. Run
  `netlify db migrations reset` to delete it, update `db/schema.ts` to reflect your new intent, then run
  `npx drizzle-kit generate` to produce a fresh migration. Editing SQL or snapshot files directly can
  desync Drizzle Kit's internal state and produce broken migrations on the next generate.

### `netlify db migrations reset` — cleaning up unapplied migrations

`netlify db migrations reset` deletes local migration files that have **not yet been applied** to your preview
branch. Applied migrations and their data are left alone.

```bash
netlify db migrations reset
```

Typical use: after a rebase pulls in new migrations from main, you may have locally-generated migrations that
now conflict with or duplicate main's work. Running `netlify db migrations reset` clears those unapplied files
so you can regenerate cleanly.

The command is safe — it cannot remove migrations that have been applied, so it cannot produce a broken
database state.

## Connecting to the database

Use `netlify db connect` to query the database directly for troubleshooting or inspecting the current schema. Always use
the `--query` flag for one-shot execution — do NOT use interactive mode.

```bash
# Inspect the current schema
netlify db connect --query "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'"

# Check columns on a table
netlify db connect --query "SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_name = 'users'"

# Read data
netlify db connect --query "SELECT * FROM users LIMIT 10"

# Get results as JSON
netlify db connect --query "SELECT * FROM users LIMIT 10" --json
```

The database connection is read-only. You can use it to inspect the schema and read data, but you cannot insert, update,
or delete rows.

**CRITICAL: NEVER run DDL statements (CREATE, ALTER, DROP, TRUNCATE) or any other schema-mutating queries through
`netlify db connect`.** The only supported way to change the database schema is through migration files in
`netlify/database/migrations/`. Running DDL directly will cause the migration state to diverge from the actual schema,
leading to unpredictable and broken behavior.

## Common mistakes to avoid

1. **Wrong migration output directory**: Drizzle Kit defaults to `drizzle/`. You MUST set
   `out: "netlify/database/migrations"` in `drizzle.config.ts`. If migrations are in the wrong directory, they will not
   be applied.

2. **Forgetting the `@beta` dist-tag**: The Netlify Database adapter only exists on `drizzle-orm@beta` and
   `drizzle-kit@beta`. Installing `drizzle-orm` or `drizzle-kit` without the `@beta` tag pulls the `latest` releases,
   which lack the adapter and will fail. Always install with `drizzle-orm@beta` and `drizzle-kit@beta`.

3. **Missing `.js` extension in imports**: When using TypeScript with ES modules, import paths should include the `.js`
   extension (e.g. `from "./schema.js"`, `from "../../db/index.js"`).

4. **Creating tables with raw SQL when using Drizzle**: If you use Drizzle ORM, define all tables in `db/schema.ts`
   and generate migrations with `drizzle-kit generate`. Do not write raw `CREATE TABLE` SQL — the schema file is the
   source of truth.

5. **Misunderstanding `netlify db migrations reset`**: This command only deletes migration files that have
   **not yet been applied** to your preview branch. It cannot undo an already-applied migration — to change
   something that's already applied, you must roll forward with a new migration.

