Recipes

Migrations

A folder of SQL files, applied from init in development; in production, applied by you or by the platform.

A migration history is a folder of SQL files. In development, init applies them to PGlite with applyMigrations from nuxt-pglite/migrations, so the local database is built from the same files as the production one. In production, either you apply them (from the build command or a CI job), or the platform does, from the files committed with your app.

server/pglite.config.ts
import { fileURLToPath } from 'node:url'

import { applyMigrations } from 'nuxt-pglite/migrations'

// Absolute, so that it does not depend on where `nuxt dev` is started from.
const migrations = fileURLToPath(new URL('./database/migrations', import.meta.url))

export default definePGliteServerConfig({
  init: (pg) => applyMigrations(pg, migrations),
})

init runs once per created instance, the socket's included, so the database is brought up to the files before anything queries it. Each migration runs in its own transaction together with its tracking row, and applyMigrations resolves with the names it applied. Add a migration, restart nuxt dev, and it is applied.

A relative directory is resolved from process.cwd(), which is why the example builds an absolute one from the config file's location. With the in-process server (server.enabled), init also runs in the built app, where the files are not shipped: keep it under $development there, or pass a directory that exists at runtime.

The config file is loaded with the app's aliases, so #pglite/migrations imports the same entry as nuxt-pglite/migrations.

Generating the files

Any tool that writes plain SQL files works. A migration is either a <name>/migration.sql directory or a flat <name>.sql file; anything else in the directory is ignored, and migrations apply in name order.

drizzle-kit 1.0.0-rc.4 and later writes <timestamp>_<name>/migration.sql next to a snapshot.json:

drizzle.config.ts
import { defineConfig } from 'drizzle-kit'

export default defineConfig({
  schema: './server/database/schema.ts',
  out: './server/database/migrations',
  dialect: 'postgresql',
  dbCredentials: {
    // the development socket, for `studio` and `check` while `nuxt dev` runs
    url: 'postgres://postgres@127.0.0.1:5433/postgres',
  },
})
Terminal
npx drizzle-kit generate --name add_visits
server/database/migrations
20261008032022_init/
  migration.sql
  snapshot.json
20261012093000_add_visits/
  migration.sql
  snapshot.json

Against the development socket, prefer the files to drizzle-kit push: a pushed schema is not what production will be built from.

In production

The same files reach the real database in one of two ways, depending on your platform. See Deploy for how each fits with the rest of the setup.

  • You apply them: most hosts only run your build. Run your tool's migrate command with the production DATABASE_URL, in the build command or a CI job, before the new code goes live: drizzle-kit migrate, or applyMigrations itself, below. See Any Postgres.
  • The platform applies them: some platforms apply the SQL files committed with your app on deploy. Point out at the directory they read, and never run push or migrate against their database: the platform records what it applied, and a schema changed behind its back is one the next deploy's migrations run against blindly. See Netlify.

Applying them yourself

Any database with exec, query and transaction works, PGlite as is. fromPool adapts a pg pool, so the files init applies locally are applied to Postgres by the same code, with the same bookkeeping:

scripts/migrate.ts
import { Pool } from 'pg'
import { applyMigrations, fromPool } from 'nuxt-pglite/migrations'

const pool = new Pool({ connectionString: process.env.DATABASE_URL })

try {
  console.log(await applyMigrations(fromPool(pool), 'server/database/migrations'))
} finally {
  await pool.end()
}

Concurrent appliers on the same database (two dev processes, a CI job and a deploy) take turns behind an advisory lock. The entry depends on Node built-ins only.

The tracking table

Applied migrations are recorded in netlify.migrations by default. That is Netlify's bookkeeping, so that a Netlify deploy and the local database agree on what was applied; any other platform just sees a tracking table. table renames it:

server/pglite.config.ts
export default definePGliteServerConfig({
  init: (pg) => applyMigrations(pg, migrations, { table: 'public.schema_migrations' }),
})

A tool with its own bookkeeping, such as drizzle-kit migrate (drizzle.__drizzle_migrations), keeps it separately: each database is tracked by the tool that migrates it, from the same files.

Options

await applyMigrations(pg, migrations, {
  table: 'netlify.migrations', // the tracking table, `schema.table` or `table`
  digests: true, // the drift check, see Edited migrations; a string names another table
  target: '20261008032022', // stop at this migration, by full name or by the part before an `_`
  logger: console, // anything with `info` and `warn`; silent by default
})

readMigrations(dir) lists the migrations as applyMigrations sees them, for tooling.

A failing migration throws a MigrationError, with the migration name and the database error as cause, once its transaction is rolled back; the migrations after it are not run. Thrown from init, it leaves the socket refusing clients with that message until the cause is fixed.

Testing a migrated database

A test database from nuxt-pglite/testing runs the config's init, so the migrations are applied to it as they are to the development one, and tests run against the schema the files produce. Build the directory from the config file's location, as above: a test runner's working directory is not always the app's. The config file is loaded outside Nuxt there, where #pglite/migrations needs alias; importing from nuxt-pglite/migrations works in both places.

To start each test from the migrated schema, apply them once and fork per test, rather than creating a database per test.