Skip to content

Migrations

Both Neon.Project and Neon.Branch accept a migrations prop — a folder of .sql files (or a Drizzle.Schema resource) 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", {
migrations: "./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 migrations directory and only migrations added after the fork are applied:

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

Files are discovered recursively under the migrations directory 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 Alchemy’s __alchemy_migrations table (created automatically), one row per file:

CREATE TABLE IF NOT EXISTS "__alchemy_migrations" (
id SERIAL PRIMARY KEY,
hash text NOT NULL,
created_at bigint,
name text,
applied_at timestamp with time zone DEFAULT now()
);

A database previously migrated with drizzle-kit or Prisma is adopted automatically: its old tracking table’s history is copied in once and the old table is frozen. Projects deployed by older Alchemy versions (which used a neon_migrations table) are upgraded in place.

On each deploy, files whose names already appear in the table are skipped. Use migrations: { dir, table } to put the bookkeeping in a differently-named table:

const project = yield* Neon.Project("my-project", {
migrations: { dir: "./migrations", table: "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", {
migrations: "./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: