# nessie: versioned Deno migrations that run inside a transaction

> Nessie is a Deno migration tool for SQLite, Postgres and MySQL whose migrations are TypeScript modules receiving a standard @stdx/database client, so application helpers and query builders work inside them. Each migration commits together with its record in the migrations table, and edited files are caught by SHA-256 checksums.

**lmxrd/deno-nessie** — A modular Deno library for PostgreSQL, MySQL, MariaDB and SQLite migrations

- Repository: https://github.com/lmxrd/deno-nessie
- Stars: 520 · Forks: 31
- Language: TypeScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/lmxrd-deno-nessie

## Versioned migrations for Deno across four database engines

Nessie is a database migration tool for Deno, built on @stdx/database, and it runs versioned up and down migrations and seeds on SQLite, Postgres and MySQL. The design decision that makes it more than a wrapper is in the second half of that sentence: migrations receive a standard @stdx/database client, so any SQL, query builder or helper written for that interface works inside them. That means a migration is not a bespoke DSL with its own escaping rules. It is a TypeScript module with an `up` and a `down` function, and the database access inside it is the same client you would use in application code. The primary language is TypeScript, the licence is MIT, and the current release line is 3.0.0, with rc.1, rc.3 and rc.4 tagged in the first week of October 2026.

## Getting started is four commands and no project scaffolding

The quick start does not ask you to create a project or install anything:

```sh
deno run -A jsr:@halvardm/nessie/cli init --dialect sqlite
deno run -A jsr:@halvardm/nessie/cli make:migration --name create_users
# edit db/migrations/<timestamp>_create_users.ts
deno run -A jsr:@halvardm/nessie/cli migrate
```

The first command initialises the configuration and the migration and seed folders for a chosen dialect, the second generates a timestamped migration file, the third is where you write your SQL, and the fourth applies everything. If you would rather have the command on your path, the install route is:

```sh
deno install -g -A -n nessie jsr:@halvardm/nessie/cli
nessie init --dialect postgres
```

Both routes run the same CLI from JSR as `@halvardm/nessie/cli`. The `-A` flag grants all permissions, which the README later explains is the simplest option, since the tool needs to read and write migration files and reach the database over the network for Postgres and MySQL.

## Flags everywhere, and positional arguments are now an error

The CLI section is explicit about a change that will bite anyone following an older tutorial. Every command except `init` accepts `-c, --config <path or URL>` with a default of `./nessie.config.ts`, and `-d, --debug`, and options are written after the command. Nessie has no positional arguments, because what a command needs is a flag with a short alias. The old positional form, for example `nessie migrate 2`, fails with an error instead of being ignored. That is a deliberate choice rather than an accident, and it makes the CLI unambiguous at the cost of breaking scripts written against 2.x. The command table shows the surface:

```sh
nessie make:migration --name create_users   # -n
nessie make:seed -n users
nessie migrate --amount 2                   # -a, the next two migrations
nessie rollback -a all                      # -a, every applied migration
nessie seed --matcher users                 # -m, a file name or a RegExp
nessie <command> --help                     # all options of a command
```

The `make` commands additionally take `--folder <dir>` to pick among several configured folders, `--force` to overwrite an existing file, and `--migration-template` or `--seed-template` to control the generated code.

## The config file is a typed object with a client and two folders

The default config file exports a `NessieConfig`:

```ts
import type { NessieConfig } from "jsr:@halvardm/nessie";
import { SqliteClient } from "jsr:@halvardm/nessie/clients/sqlite";

const config: NessieConfig = {
  client: new SqliteClient("./sqlite.db"),
  migrationFolders: ["./db/migrations"], // default
  seedFolders: ["./db/seeds"], // default
};

export default config;
```

The `client` option is required and takes anything implementing `NessieClient`, with `SqliteClient` as the example. `migrationFolders` and `seedFolders` default to `./db/migrations` and `./db/seeds`. The other options extend that in directions that matter for real projects. `additionalMigrationFiles` and `additionalSeedFiles` accept anything `import()` accepts, including URLs, so migrations can live outside the project. `migrationTemplate` and `seedTemplate` point at a path or URL used by the `make` commands, which is how a team enforces a house style on generated migrations. `debug` is the same as the `--debug` flag. The examples directory shows configurations for Postgres, MySQL, remote files, a custom client and use as a library.

## A migration is an up and a down function over a transaction

The migration format is small. A migration is the default export of `<yyyyMMddHHmmss>_<name>.ts`, and the shape is an object with an `async up` and an `async down`:

```ts
import type { Migration } from "jsr:@halvardm/nessie";

export default {
  async up({ client }) {
    await client.execute(
      "CREATE TABLE users (id INTEGER PRIMARY KEY, name VARCHAR(100) NOT NULL)",
    );
  },
  async down({ client }) {
    await client.execute("DROP TABLE users");
  },
} satisfies Migration;
```

The important guarantee is what surrounds the migration. Each migration runs in a transaction together with its entry in the `nessie_migrations` table, so a failing migration is not recorded. That is the property that separates a migration tool from a script that runs SQL files, and it is the one to check first when choosing a tool. The `client` handed to your functions is an @stdx/database `Queryable`, with `execute`, `query`, `executeScript`, `prepare` and nested `transaction`s, and there is also a `dialect` object describing the database, with `dialect.name`, `dialect.placeholder(i)` and `dialect.quoteIdentifier()` for SQL that differs between engines.

## MySQL commits DDL implicitly, which the README does not soften

There is one database-specific caveat and the README is blunt about it. Set `transaction: false` on a migration for statements that can not run in a transaction, because MySQL commits DDL statements implicitly, so a migration changing the schema can not be rolled back there, with or without a transaction. That is a property of MySQL rather than a bug in the tool, but it means the transaction guarantee that protects Postgres and SQLite does not extend to MySQL, and a failed MySQL migration can leave the schema half changed. The `dialect` object exists for exactly this class of problem, letting a migration branch on the engine when the SQL differs, and the examples directory includes a file dedicated to migration dialects.

## Checksums catch a migration file that was edited after being applied

Nessie stores a SHA-256 checksum of each migration file when it is applied. If a file is edited afterwards, `nessie migrate` and `nessie status` print a warning, and `status` reports the affected entries under `modifiedMigrations`. The `status` command is the one to know about, since it shows the available, applied, pending and modified migrations, with `--output json` and `--file-names` options for scripting. The checksum behaviour is a good deal more than a nicety, because the usual failure mode of a migration system is a developer editing an already-applied migration so that a fresh database and a migrated database end up with different schemas, and the checksum is what turns that from a silent divergence into a warning.

## Where Nessie is the wrong tool, and how a SQL migrator differs

Three limits are concrete. The first is that it is a Deno tool, not a Node tool. The CLI is invoked with `deno run` and published on JSR, and the client comes from @stdx/database, so a Node or Bun service that wants the same migrations is looking at a different ecosystem, and the tool does not offer a bridge. The second is the release state. The current line is 3.0.0 and it is at release candidate 4, with rc.1, rc.3 and rc.4 in the space of a week, and the README carries a dedicated section on migrating from 2.x, which means the API changed in ways that affect a working setup. The third is the MySQL DDL behaviour described above, and it is not a workaround but a fact about the engine, so a project that must support MySQL needs to accept that a schema migration there is not atomic.

The realistic alternatives are a SQL-first migrator such as dbmate or a migration tool from the language's own ecosystem, and running migrations from a Node library. The difference in approach is where the migration file lives. Nessie makes it a TypeScript module, so a migration can import your application code, use your query builder, call your validation helpers and share types with the rest of the codebase, which is the reason the README emphasises that the client is a standard @stdx/database one. A SQL-first migrator puts the migration in a `.sql` file, which is easier to review and easier to run in a pipeline that has no application runtime, and it cannot do anything that SQL cannot express. The trade is between a migration that can reason in your application's language and a migration that is pure data and trivially portable. Nessie also supports being used as a library, so the CLI is not the only way in.

## Conclusion

nessie fits a Deno team that wants migrations to be ordinary TypeScript modules sharing the same database client as the application, and that needs the applied migration record to commit atomically with the schema change. It is the wrong choice for a Node service, since the CLI runs on Deno and the client comes from @stdx/database, and the wrong choice if you need atomic DDL on MySQL, which commits implicitly. Before you adopt it, check the 3.0.0 release candidate notes against the 2.x migration guide in the README, and confirm whether your MySQL migrations can tolerate a non-transactional schema change.

## FAQ

### How do I install and run nessie?

Run `deno run -A jsr:@halvardm/nessie/cli init --dialect sqlite` to create the config and folders, then `make:migration` to generate a file and `migrate` to apply it. To get the command on your path, use `deno install -g -A -n nessie jsr:@halvardm/nessie/cli`.

### What does a nessie migration file look like?

It is the default export of `<yyyyMMddHHmmss>_<name>.ts` with an `async up` and an `async down`, each receiving a client that is an @stdx/database `Queryable` with `execute`, `query`, `executeScript`, `prepare` and nested `transaction`s.

### Does nessie roll back a failed migration?

Each migration runs in a transaction together with its entry in the `nessie_migrations` table, so a failing migration is not recorded. The exception is MySQL, which commits DDL statements implicitly, so a schema migration there can not be rolled back with or without a transaction, and `transaction: false` is available for statements that can not run in one.

### How does nessie detect an edited migration?

It stores a SHA-256 checksum of each migration file when it is applied. If the file is edited afterwards, `nessie migrate` and `nessie status` print a warning, and `status` lists the affected entries under `modifiedMigrations`.

## Sources

- [Issues](https://github.com/lmxrd/deno-nessie/issues)
- [License: MIT](https://github.com/lmxrd/deno-nessie/blob/main/LICENSE)
- [lmxrd/deno-nessie on GitHub](https://github.com/lmxrd/deno-nessie)
- [README](https://github.com/lmxrd/deno-nessie/blob/main/README.md)
- [Releases](https://github.com/lmxrd/deno-nessie/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/lmxrd-deno-nessie
