@bydey/tusk
v1.0.0
Published
SQL-first PostgreSQL migrations for Node.js and Bun
Maintainers
Readme
Tusk
Tusk is a SQL-first PostgreSQL migration tool for Node.js and Bun.
It is explicit and low abstraction:
- write plain SQL, not a migration DSL
- define
upanddownsteps directly
It handles execution for you:
- transactional migrations with advisory locking
- one migration contract across
pg,postgres.js, and the CLI
Tusk favors control and embeddability over schema builders and generated migrations.
Requirements
- Node.js
18+or Bun1.3.8+ - PostgreSQL
13+ - ESM projects for programmatic use; the CLI works from any project type
Recommended for new projects:
- Node.js
24 - PostgreSQL
18
Install
Install Tusk with one PostgreSQL driver. With pg:
npm install @bydey/tusk pg
# or
bun add @bydey/tusk pgWith postgres.js:
npm install @bydey/tusk postgres
# or
bun add @bydey/tusk postgresThe package root contains the driver-neutral migration API. Import driver and framework integrations from their explicit subpaths:
import { runUp } from "@bydey/tusk";
import { createPgAdapter } from "@bydey/tusk/pg";
// or: import { createPostgresJsAdapter } from "@bydey/tusk/postgres";The Elysia integration requires both elysia and pg:
bun add @bydey/tusk elysia pgThe CLI and MCP server discover the installed driver. If both are installed,
pg is the default; set TUSK_DRIVER=postgres to select postgres.js.
Migration Model
Tusk uses timestamped SQL files with explicit directionality.
Tusk reads SQL files from a migrations directory:
migrations/
1728123456789_create_users.up.sql
1728123456789_create_users.down.sql*.up.sqlis applied when you runup*.down.sqlis applied when you rundown- executed migrations are tracked in the
_migrationstable - migrations are protected by a Postgres advisory lock so two runners do not apply them at the same time
Tusk does not provide a migration DSL or schema abstraction layer.
The _migrations table name and shape are part of Tusk's v1 compatibility
contract. See Metadata table contract for the exact
schema, search_path scope, and safety behavior.
Example:
-- migrations/1728123456789_create_users.up.sql
CREATE TABLE users (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL UNIQUE
);-- migrations/1728123456789_create_users.down.sql
DROP TABLE IF EXISTS users;Quick Start
This is the recommended first migration flow.
- Create a
.envfile. Tusk loads it automatically. Set eitherDATABASE_URL:
DATABASE_URL=postgresql://user:password@localhost:5432/appor individual settings:
DB_HOST=localhost
DB_PORT=5432
DB_NAME=app
DB_USER=postgres
DB_PASSWORD=secretOptional settings:
MIGRATIONS_PATH=./migrations
LOG_LEVEL=warn
TUSK_DRIVER=pg
TUSK_STATEMENT_TIMEOUT_MS=300000TUSK_STATEMENT_TIMEOUT_MS applies to each migration transaction. Use 0 to
leave PostgreSQL's statement timeout unchanged. See Transactions and
timeouts.
- Initialise the project and create a migration pair:
npx tusk init
npx tusk create create_usersWith Bun:
bunx tusk init
bunx tusk create create_users- Edit both generated files under
migrations/:
<timestamp>_create_users.up.sql
<timestamp>_create_users.down.sql- Validate the files before connecting to the database:
npx tusk validate- Run the read-only project and database preflight:
npx tusk doctor- Review the ordered SQL without applying it:
npx tusk up --dry-run- Apply the migration, then confirm status:
npx tusk up
npx tusk statusFor CI and agents, add --json. Use tusk status --exit-code to exit 1 when
migrations are pending, or tusk status --quiet for a single summary line.
Roll back migrations. down defaults to one rollback so an omitted
argument cannot accidentally undo the full migration history:
npx tusk down
npx tusk down 1
npx tusk down --dry-run
npx tusk down 3
npx tusk down --allRollback contract:
tusk downandtusk down 1roll back exactly the latest applied migration.tusk down nrolls back the latestnapplied migrations, newest first.tusk down --allrolls back every applied migration, newest first.- An adopted
0000000000000_initial.up.sqlbaseline is protected from ordinary rollback. Tusk refuses before executing the batch unless the destructive--allow-baseline-rollbackoverride is present. - If
nis greater than the number of applied migrations, Tusk rolls back all available applied migrations and says how many were available. - If no migrations are applied, Tusk exits successfully with
No applied migrations to roll back. - Rollbacks use only matching
.down.sqlfiles. If any required rollback file is missing, Tusk fails while planning before applying the rollback batch.
Starting From an Existing Database
For a new project, initialise the local migration directory first:
npx tusk initThis creates ./migrations when it is missing. Add matching .up.sql and
.down.sql files there, then run tusk doctor and tusk up.
If your schema already exists, Tusk can create and record a starting baseline with the explicit adoption flag:
npx tusk init --from-dbThis creates 0000000000000_initial.up.sql and
0000000000000_initial.down.sql so future schema changes can be managed through
normal migrations.
The generated initial migration is recorded in _migrations as already
applied, so tusk up skips it and new migrations run normally. Ordinary
rollback will not remove this baseline. The explicit
--allow-baseline-rollback override can drop every table represented by the
baseline and should be treated as destructive.
Adoption is not a complete PostgreSQL backup or schema-dump replacement. Tusk fails before writing or recording a baseline when it detects schema features it cannot reproduce safely, including custom types, arrays, views, routines, triggers, policies, generated columns, check/exclusion constraints, partitions, inheritance, and unowned sequences. See Existing database adoption for the exact boundary.
Programmatic Use
With pg:
import { Pool } from "pg";
import { runUp } from "@bydey/tusk";
import { createPgAdapter } from "@bydey/tusk/pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = createPgAdapter(pool);
try {
await runUp(adapter, "./migrations");
} finally {
await pool.end();
}With postgres.js:
import postgres from "postgres";
import { runUp } from "@bydey/tusk";
import { createPostgresJsAdapter } from "@bydey/tusk/postgres";
const sql = postgres(process.env.DATABASE_URL!);
const adapter = createPostgresJsAdapter(sql);
try {
await runUp(adapter, "./migrations");
} finally {
await sql.end();
}Generate an initial migration programmatically:
import { Pool } from "pg";
import { createInitialMigration } from "@bydey/tusk";
import { createPgAdapter } from "@bydey/tusk/pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const adapter = createPgAdapter(pool);
try {
await createInitialMigration(adapter, "./migrations");
} finally {
await pool.end();
}Custom database clients only need to implement the exported
MigrationAdapter contract (query, transaction, and migration lock
methods) to use planning and execution APIs. DatabaseAdapter extends that
contract with introspection and SQL generation used only by
createInitialMigration. See the custom adapter contract
before implementing one.
Elysia
import { Elysia } from "elysia";
import { migrate } from "@bydey/tusk/elysia";
new Elysia()
.use(
migrate({
connectionString: process.env.DATABASE_URL,
migrationsPath: "./migrations",
}),
)
.listen(3000);By default the plugin runs up on startup.
The Elysia integration requires Bun 1.3.8+ or Node.js 20+; the rest of Tusk
supports Node.js 18+.
CLI Commands
tusk create <name>
tusk init
tusk init --from-db
tusk up
tusk down [count]
tusk down --all
tusk down --allow-baseline-rollback
tusk status
tusk validate
tusk doctor
tusk versiontusk down and tusk down 1 roll back one migration by default. This is
intentionally narrow: the safest rollback is the latest migration only, and
broader rollback should be explicit. Use tusk down <count> to roll back
several migrations, or tusk down --all to roll back every applied migration.
Rollback batches run newest first and use only matching .down.sql files. If a
requested count is larger than the applied migration count, Tusk rolls back all
available applied migrations and reports the smaller available count.
Adopted baselines are protected. If a selected rollback would include
0000000000000_initial.up.sql, Tusk refuses the whole batch unless
--allow-baseline-rollback is supplied. Programmatic callers must opt in with
a rollback target such as
{ count: 1, allowBaselineRollback: true }.
tusk up --dry-run, tusk down --dry-run,
tusk down <count> --dry-run, and tusk down --all --dry-run print the
ordered migration SQL without applying it.
tusk status --exit-code exits with status 1 when migrations are pending and 0 when the schema is clean.
tusk status --quiet suppresses the detailed sections and prints only the summary line, which is useful for scripts and CI logs.
tusk status --json prints machine-readable status data with ok, command, executed, pending, and summary fields. It can be combined with --exit-code, but not with --quiet.
tusk validate checks migration filenames, pairs, executable SQL, duplicate timestamps, and transaction-control statements. Add --db to check executed migration checksums against the configured database without modifying migration state.
tusk doctor runs a read-only health check over the migration directory, database configuration, connection, PostgreSQL compatibility, migration metadata, checksum drift, status readability, and advisory lock support. It exits 0 when there are no failing checks and 1 when action is needed. Use tusk doctor --json for automation.
--json is supported by create, init, up, down, status, validate, and doctor for machine-readable automation output. See the JSON output contracts for stable fields and compatibility rules.
Agent and MCP Use
For AI agents and automation, prefer the safe loop in Agent workflow: doctor --json, validate --json, validate --db --json, up --dry-run --json, then apply only after the plan is reviewed.
Tusk also includes a stdio MCP server:
tusk-mcpIt exposes tools for validation, status, dry-run planning, and migration file creation.
Support Policy
Tusk keeps a wide support floor for teams working on older projects while still treating the current stack as the primary development lane.
- Supported floor: Node.js
18+, Bun1.3.8+, PostgreSQL13+ - Recommended stack: Node.js
24, PostgreSQL18 - Required PR CI checks:
Verify (Node 24, PostgreSQL 18)requires the full build/test/quality lane and the globally aggregated exhaustive mutation suiteMinimum Support (Node 18, PostgreSQL 13)runs the packaged smoke test against the oldest supported runtime/database pairPackage smoke (macos-latest)andPackage smoke (windows-latest)verify the installed artifact on both desktop platforms
- Scheduled compatibility coverage:
- the
Compatibility Matrixworkflow exercises packaged smoke tests across multiple supported Node.js and PostgreSQL versions
- the
If a supported floor version stops passing CI, it is a regression and should be treated as a bug.
More
- Runnable basic example
- Framework integrations
- Transactions and timeouts
- Existing database adoption
- Compatibility policy
- Doctor command
- JSON output contracts
- Testing guide
- Agent workflow
- Release guide
- v1.0.0 readiness
- v1 release checklist
- v0.5.0 release notes
- v0.3.0 release notes
License
MIT
