Skip to content

Migrations

Both Neon.Project and Neon.Branch accept a migrationsDir — a folder of .sql files applied in order as part of every deploy. On a project, migrations run against the default branch’s primary database; on a branch, they run against the branch itself:

const project = yield* Neon.Project("my-project", {
migrationsDir: "./migrations",
});

Because branches are copy-on-write forks, a branch created from a migrated parent already contains the parent’s schema — and its tracking table. Point the branch at the same migrationsDir and only migrations added after the fork are applied:

const featureBranch = yield* Neon.Branch("feature", {
project,
migrationsDir: "./migrations",
});

Files are discovered recursively under migrationsDir and sorted by their numeric prefix (0001_init.sql, 0002_users.sql, …), falling back to name order for files without one. Each file’s contents are SHA-256 hashed and the hashes are persisted in the resource’s state (migrationsHashes), so adding a migration file — or editing an existing one — is what marks the resource for an update on the next deploy. When nothing changed, the migration step is skipped entirely.

Applied migrations are recorded in a neon_migrations table (created automatically) with one row per file:

CREATE TABLE IF NOT EXISTS "neon_migrations" (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

On each deploy, files whose names already appear in the table are skipped. Use migrationsTable to point at a different table — for example one written by another migration tool:

const project = yield* Neon.Project("my-project", {
migrationsDir: "./migrations",
migrationsTable: "my_migrations",
});

Each pending migration runs inside a transaction together with its bookkeeping INSERT into the tracking table, so partial application is impossible: a failing statement rolls the whole file back and fails the deploy, while migrations that already committed stay applied. Fix the file and re-deploy — the run resumes from the failed migration.

importFiles lists additional .sql files to apply after migrations. Paths are resolved relative to the working directory:

const project = yield* Neon.Project("my-project", {
migrationsDir: "./migrations",
importFiles: ["./seed/users.sql"],
});

Unlike migrations, import files are not recorded in the tracking table. Each file is content-hashed and re-applied whenever its contents change, so write them to be safe to run more than once (INSERT ... ON CONFLICT DO NOTHING, CREATE OR REPLACE, and the like). Files whose hashes match the previous deploy are skipped.

Guides:

Related:

  • Neon overview — projects, branches, and composing with your cloud.
  • Setup — credentials and provider registration.

Reference: