1. guides
prorm
  • prorm documentation
  • Known test gaps
  • Architecture
  • Core concepts
  • Database types & coverage
  • Getting started
  • Going further
  • compliance
    • Compliance
    • The security toolkit: `prorm audit`
    • Checklists by regulation
    • Declaring personal data
    • Evidence bundles
    • Evidence
    • Frameworks: `@Security`
    • Jurisdictions
    • Platform policies
    • Protecting data
    • The regulation dataset
    • Residency and cross-border transfers
    • Retention & integrity
    • Status
    • Subject rights
  • decorators
    • prorm with decorators
    • Models with decorators
    • Querying a decorated model
    • Relations with decorators
  • diagrams
    • Diagram generators
    • Chen diagram
    • Class diagram
    • Dependency diagram
    • ER diagram
    • Flow diagram
    • From your models to a diagram
    • Gantt diagram
    • Index diagram
    • Migration diagram
    • Model diagram
    • Package diagram
    • Crow’s-foot diagram
    • Schema documentation
    • Sequence diagram
    • State diagram
    • Tree diagram
  • dialects
    • SQL dialects
    • AlloyDB Dialect
    • AlloyDB Omni Dialect
    • Amazon Athena Dialect
    • Aurora DSQL Dialect
    • Amazon Aurora MySQL Dialect
    • Amazon Aurora PostgreSQL Dialect
    • Azure Database for MySQL Dialect
    • Azure Database for PostgreSQL Dialect
    • Azure SQL Database Dialect
    • Azure SQL Managed Instance Dialect
    • Google BigQuery Dialect
    • Citus Dialect
    • ClickHouse dialect
    • Cloud Authentication (`cloud-auth`)
    • Cloud SQL for SQL Server Dialect
    • Cloud SQL for MySQL Dialect
    • Cloud SQL for PostgreSQL Dialect
    • CockroachDB Dialect
    • Azure Cosmos DB for PostgreSQL Dialect
    • CrateDB Dialect
    • Cloudflare D1 Dialect
    • Databricks dialect
    • IBM Db2 Dialect
    • Dolt Dialect
    • Apache Doris Dialect
    • Druid Dialect
    • DuckDB Dialect
    • Exasol dialect
    • Microsoft Fabric Data Warehouse Dialect
    • Firebird Dialect
    • Greenplum Dialect
    • SAP HANA Dialect
    • MySQL HeatWave Dialect
    • Azure Data Explorer (Kusto) Dialect — T-SQL
    • MariaDB Dialect
    • Materialize Dialect
    • MonetDB Dialect
    • MotherDuck Dialect
    • Microsoft SQL Server Dialect
    • MySQL Dialect
    • Neon Dialect
    • OceanBase Dialect
    • openGauss Dialect
    • Oracle Dialect
    • Oracle Autonomous Database Dialect
    • ParadeDB Dialect
    • PolarDB Dialect
    • PostgreSQL Dialect
    • Presto Dialect
    • QuestDB Dialect
    • Aurora RDS Data API Dialect (`rdsdata`)
    • Amazon RDS for Db2 Dialect
    • Amazon RDS for MariaDB Dialect
    • Amazon RDS for SQL Server Dialect
    • Amazon RDS for MySQL Dialect
    • Amazon RDS for Oracle Dialect
    • Amazon RDS for PostgreSQL Dialect
    • Amazon Redshift
    • Amazon Redshift Data API Dialect
    • RisingWave Dialect
    • rqlite Dialect
    • SingleStore Dialect
    • Snowflake Dialect
    • Google Cloud Spanner Dialect
    • Spanner (PostgreSQL interface) Dialect
    • SQLite Dialect
    • StarRocks Dialect
    • Supabase Dialect
    • Azure Synapse Analytics (dedicated SQL pool)
    • Teradata Dialect
    • TiDB Dialect
    • TimescaleDB Dialect
    • Trino Dialect
    • Turso / libSQL Dialect
    • Vertica dialect
    • Vitess Dialect
    • Yellowbrick Dialect
    • YugabyteDB Dialect
  • guides
    • Access rules and behaviour profiles
    • ArangoDB (AQL)
    • Associations
    • Audit levels and escalation
    • Audit logging
    • In the browser
    • Bulk operations
    • Caching
    • Capabilities, tiers and team roles
    • CLI
    • Cloudflare
    • Compliance & data protection
    • Connection Pooling & Performance
    • Security in the console
    • Couchbase: structured N1QL queries
    • Data types
    • Decorators
    • Eager loading
    • Enums and constants
    • Error handling & error types
    • Extensions
    • External fields
    • Foreign data wrappers
    • Graph databases
    • Hooks & lifecycle events
    • Indexes & constraints
    • InfluxDB: structured Flux pipelines
    • klock-errors
    • klock — the `.klock` file
    • Local databases (`prorm db up`)
    • Structured log search — Splunk, Graylog, Papertrail, Sumo Logic
    • Logging
    • Grafana Loki: structured LogQL queries
    • MCP server
    • Migrate — the data transfer subsystem
    • Migrations & Schema Sync
    • Models from an existing database
    • Defining models
    • Multiple databases
    • OrientDB, Neptune and TigerGraph
    • Passwords
    • Importing a Prisma schema
    • Structured PromQL and MetricsQL
    • Query firewall
    • QueryInterface
    • Query operators reference
    • Query optimization
    • Composing find options with `pipe()` and `Q`
    • Query safety: the fail-closed WHERE guard
    • Querying
    • Raw Queries & Query Building
    • Read replicas
    • Schema diffing & migration generation
    • Views, triggers & other schema objects
    • Scopes & default scopes
    • Seeders
    • SPARQL / RDF triplestores
    • SQL constants
    • SQL features
    • SQL function builders
    • SQLite advanced features
    • StoreKey — a prod-orm database as the storage
    • Streaming results
    • Terraform
    • Transactions & Isolation Levels
    • Type mapping: legacy and modern
    • TypeScript types
    • Database users, roles & privileges
    • Validation & constraints
    • Virtual & Computed Fields
    • Writing docs
  • javascript
    • prorm in JavaScript
    • Models in JavaScript
    • Querying in JavaScript
    • Relations in JavaScript
  • runbooks
    • Operational runbooks
    • Connection failures
    • Credential rotation
    • Failover
    • Incident checklist
    • Deploying klock v2
    • Migration rollback
  • running-databases
    • Running databases in Docker
    • Running prorm under Bun in Docker
    • Running Cassandra in Docker with prorm
    • Running ClickHouse in Docker with prorm
    • Running CockroachDB in Docker with prorm
    • Running prorm under Deno in Docker
    • Running Elasticsearch in Docker with prorm
    • Running MariaDB in Docker with prorm
    • Running MongoDB in Docker with prorm
  • prorm control plane
    • Introduction
    • Authentication
    • Errors
    • Getting started
      • Service
        • Is it up
        • This description
    • Account
      • Sign-up and verification
        • Create an account
        • Exchange credentials for a token
        • Verify with the emailed code
        • Verify by clicking the emailed link
        • Name the parent or guardian
        • What a consent link is about
        • Give consent for a child's account
        • Ask the parent or guardian again
        • Send a new code and link
      • Family
        • Everyone this account is responsible for
        • One child, in detail
        • Turn a limit on or off
      • Profile
        • Who am I
        • Change the account
        • Every address on the account
        • Add an address
        • Confirm an address with the code mailed to it
        • Mail a fresh code to an unconfirmed address
        • Make an address primary
        • Remove an address
        • What this caller may do
    • Security
      • Sessions
        • Everywhere this account is signed in
        • Sign out everywhere else
        • Sign one out
        • A token for the CLI on a named machine
        • A token for the customer console, from the staff one
        • Park a sign-in for the CLI to collect
        • The CLI collecting its sign-in
        • The team's members' live sessions
        • Sign a member's session out
        • Sign an account out everywhere
        • Everywhere the account is signed in
        • Sign an account's session out
      • Single sign-on
        • Begin a single sign-on
        • The provider's return
        • Link the signed-in account to a team's provider
        • Which team an address signs in to
        • The team's single sign-on, as a member sees it
        • Set the team's OpenID Connect provider up, or change it
        • Remove the provider
        • The team's domain claims and the DNS record each needs
        • Prove a claimed domain
      • MFA and passwords
        • Start enrolling a second factor
        • Prove the app has it
        • Remove the second factor
        • Ask for a reset link
        • Set a new password with the emailed token
      • Klock
        • Open a device-bound klock session
        • Check in, for the next grant
        • Step up the session asking
        • Rotate the device key
        • Freeze a session
        • Unfreeze a session
        • A guest grant
        • The revocation feed
        • The published keyset
        • What this control plane supports
      • Devices
        • Your devices, or a team's
        • Approve a new device's pairing code
        • One device
        • Rename a device
        • Revoke a device
        • Suspend a device
        • Reinstate a suspended device
        • Wipe a device
        • Ask a device to rotate its key
        • The team's members' devices
        • Revoke a member's device
        • An account's devices
        • Revoke an account's device
        • Wipe an account's device
        • The device fleet
      • Access rules and behaviour
        • The access rules that bear on you
        • Add an access rule
        • This request, as the rules see it
        • One access rule
        • Remove an access rule
        • The behaviour profiles that bear on you
        • Add or change a behaviour profile
        • The profile presets
        • Your behaviour findings
        • Acknowledge a finding
        • One behaviour profile
        • Remove a behaviour profile
        • The team's access rules
        • Add a team access rule
        • Remove a team access rule
        • The team's behaviour profiles
        • Add or change a team behaviour profile
        • Remove a team behaviour profile
        • What the team's profiles caught
        • Acknowledge a team finding
      • Recovery and lockdown
        • Ask for a recovery link
        • Recover access with the emailed code
        • Is the account locked down
        • Lock the account down
        • Release a lockdown
        • Lock a member down
        • Release a member's lockdown
      • MCP tokens
        • Make a delegated token for one MCP client
        • The account's delegated tokens
        • Revoke a delegated token
        • What a delegated token may do now
        • Run one MCP tool with a delegated token
        • Run a device-signed MCP tool call relayed by the MCP Worker
      • Kerberos
        • Give the team its own Kerberos realm
        • The team's realm
        • Remove the team's realm
    • Logging and monitoring
      • Audit log
        • What has been done to the account
        • What has happened to this account
        • The account's audit trail
      • Operations
        • Everything that has been asked of this database
        • One, with its steps
        • Stop it
        • Put a failed one back in the queue
        • Every operation, across all accounts
      • Metrics and events
        • Every measurement, for a graph
        • Who did what to it
    • Billing
      • Plans and pricing
        • What the plans allow
        • The rate card
        • What one database would cost
        • What this account is paying for, itemised
      • Subscriptions and invoices
        • What this month's invoice will say
        • What this account is on
        • What it has been charged
        • Start a Stripe Checkout session
        • Open the customer portal
        • Stripe's events
        • Buy more connection pools
      • Team billing
        • The team's billing account
        • What the team has been charged
        • What this month's invoice will say
        • The team's cards
        • Add a card, through a Stripe-hosted setup session
        • Make a card the team's default
        • Remove a card
        • Start the team paying
        • Open the Stripe portal on the team's customer
        • Change the team's plan
        • Who receives the team's bills
        • How the team pays for transfers
    • Support
      • Support chat
        • My conversations
        • Start a conversation
        • Every support conversation, open first
        • A ticket for the socket
        • One conversation, with who is in it and who is connected
        • Rename a group; open or close a support conversation
        • The lines, oldest first
        • Say something
        • Add somebody
        • Mark read
    • Administration
      • Users
        • Every account
        • One account, with everything hanging off it
        • Grant or revoke admin
        • Verify an account by hand
        • Lock or unlock sign-in
        • Allow or hold provisioning
        • Send the verification mail again
        • Set the plan by hand
      • Platform
        • Every database and store, across all accounts
        • The counts worth putting on a dashboard
      • Staff and permissions
        • What kind of admin the caller is, and which scopes they hold
        • The admin presets
        • Every admin, and the scopes each resolves to
        • Make an account an admin of a kind
        • Change an admin's kind, or a custom admin's scopes
        • Revoke admin
      • Team administration
        • Every team, with its owner and size
        • One team in full — members with their accounts, and projects
        • Hand the team to another member
        • Put an account in the team, active at once
        • Change a member's role
        • Remove a member
      • Assumed identities
        • Assume an identity — act as a customer account
        • The identities the caller holds open
        • End an assumed identity
      • Support inbox
        • The platform's addresses
        • Add a platform address
        • One address, with its counts
        • Change an address's label, routing, or whether it accepts mail
        • Remove an address that holds no mail
        • Make, adopt or repair the address's routing rule
        • Every admin's personal address, made where missing
        • File a raw message, as if it had arrived
        • The inbox
        • One message, whole
        • Mark a message
        • Hand a message to an admin, or take it back
        • Leave an internal note
        • Reply, from the address the message came to
        • What is waiting
    • Databases
      • Databases
        • Data stores
          • Every data store on the account
          • Provision a data store
          • Change a data store that is already running
          • One data store
          • Deprovision it
          • Report what it is using
          • How to reach a data store
        • Connections and credentials
          • The api keys on a database
          • Issue a key
          • Revoke a key
          • The engine users we created
          • Create a user inside the database
          • Give a user a new password
          • Drop a user
          • How the database is reached
          • Set how it is reached
          • Revoke every api key on it
          • How to connect to it
          • The ssh keys that open its bastion
          • Add a key, or have one made
          • The private half of a generated key, once
          • Revoke an ssh key
        • Schema and objects
          • The SQL objects we put in it
          • Create one, and apply it
          • What is in the database, as the engine reports it
          • One table
          • What it would run, without running it
          • Redefine an object in place
          • Drop it
          • The extensions installed in it
        • Migrations
          • A plan compiled to statements, recording nothing
          • The migrations recorded for it
          • Record one
          • Apply everything recorded and not yet run
          • Apply everything pending, oldest version first
        • Lifecycle
          • Grow or shrink it
          • Stop it accepting connections
          • Start it again
          • Move it to another region
        • Backups and replicas
          • Take a backup
          • The backups it has
          • Restore one into it
          • What has been restored into it, and from where
          • Its read copies
          • Add one
        • Policies
          • The catalog
          • One policy
          • What this database is held to
          • Switch a policy on, or change its knobs
          • Switch it off
          • Mark the policies reviewed
          • Evaluate the periodic policies now
          • Who is behind a credential
        • Passwordless
          • The database's passwordless sessions
          • Issue a passwordless session (v1)
          • Revoke a passwordless session
          • Bind a session to a device key
          • Issue a v2 session for a host to bind
          • Redeem an enrolment ticket on the host
          • Check a passwordless credential
          • Run one described query under a passwordless credential
        • Database roles
          • The database's roles
          • Define a role
          • One role
          • Change what a role permits
          • Delete a role, and revoke every key granted it
          • Grant a role, which mints a key
          • Who holds a role
          • The api keys bound to a role
        • Pools
          • A database's connection pools, and how busy each is
          • Open another pool
          • Rename a pool or change its size
          • Close a pool
        • Data plane
          • Run a query against a database
          • Several statements, in order, over one connection
          • Is this database answering
          • What the proxy itself is holding
        • Transfers
          • The strategies this account's tier may use
          • Price a transfer without creating it
          • The account's transfer jobs, newest first
          • Create a transfer job
          • One job, and whether its certificate holds
          • Cancel an unfinished job
          • Start a created job, confirming its price
          • Report a job's status (the runner, or the owner's side of a local run)
        • Every database on the account
        • Provision a database
        • Change a database that is already running
        • One database
        • Deprovision it
        • Read or write it
        • Report what it is using
        • The engines this API will provision
        • Where a managed database can be put
    • Teams
      • Teams
        • Members
          • Who is on it
          • Invite somebody
          • Accept an invitation
          • Change somebody's role
          • Remove somebody
        • Projects
          • The projects a team runs
          • Start one
          • Rename a project, or move its environment
          • Delete a project
          • One project, and what is in it
          • Put a database or store in it
          • What is in a project
          • Take one out
        • The teams this account is on
        • Start one
        • The twelve roles, and what each may do
        • Rename a team
        • Close a team
        • One team
        • Hand the team to somebody else
        • Leave a team
    • Schemas
      • Error
      • ValidationErrors
      • Token
      • PolicyCategory
      • PolicySeverity
      • PolicyAction
      • PolicyTrigger
      • PolicyParam
      • Policy
      • PolicyCatalog
      • PolicyAssignment
      • PolicyOutcome
      • PolicyFiring
      • CredentialTrace
      • Relation
      • Restrictions
      • ChildSummary
      • SignupRequest
      • SigninRequest
      • UpdateMeRequest
      • User
      • Address
      • GeoPoint
      • D1Region
      • Usage
      • UsageReport
      • Resource
      • Database
      • DataStore
      • NewDatabase
      • NewDataStore
      • QueryRequest
      • DatabasePrivilege
      • ApiKey
      • DbUser
      • Access
      • AccessRequest
      • DatabaseObject
      • NewDatabaseObject
      • NewMigration
      • SchemaOperation
      • Migration
      • Operation
      • Session
      • AdminPowers
      • Admin
      • AuditEntry
      • TeamRole
      • Conversation
      • ChatMessage
      • TeamSso
      • Team
      • TeamMember
      • SshKey
      • ConnectionInfo
      • InboxAddress
      • InboxMailbox
      • InboxMessageSummary
      • InboxMessage
      • InboxNote
      • Page
      • Quote
      • Plan
      • ApiError
      • CapabilityRefusal
      • KlockErrorCode
      • PageWindow
      • SessionView
      • KlockStepUpRequest
      • KlockOpenRequest
      • KlockOpenedSession
      • UpdateMeResult
      • CapabilitiesView
      • SessionLimits
      • Device
      • DeviceListed
      • DeviceTeamRef
      • DeviceCommand
      • DeviceAuditLine
      • DeviceActionRequest
      • StaffActionRequest
      • ManagedSession
      • SessionRevoked
      • AccessRule
      • AccessRuleRequest
      • BehaviourProfile
      • BehaviourProfileRequest
      • BehaviourProfileSaved
      • BehaviourFinding
      • BehaviourFindingAck
      • StepUpProof
      • Removed
      • MemberLockdownRequest
      • McpClient
      • McpDelegatedView
      • McpToolCall
      • McpForwardRequest
      • PasswordlessScope
      • RoleScope
      • PasswordlessSession
      • PasswordlessTlsMaterial
      • PasswordlessKerberosMaterial
      • ServiceDeviceKeys
      • ServiceSession
      • DatabaseRole
      • TransferEndpoint
      • TransferJobRequest
      • TransferQuote
      • TransferStrategy
      • TransferJob
      • TransferJobAccess
      • Pool
      • PoolAllowance
      • BillingSubscription
      • Invoice
      • BillingContact
      • PaymentMethod
      • UpcomingCharges
      • TeamBilling
      • TeamKerberosRealm
      • TeamSsoDomain
      • TeamSsoResponse
      • StoreConnection
      • BatchFailure
      • ProxyStatus
  1. guides

Migrations & Schema Sync

Prorm gives you two complementary ways to get your model definitions into a real
database schema: prorm.sync(), which reconciles tables directly from your models,
and the file-based migration system (Migrator + QueryInterface), which records
each incremental change as a versioned, reversible script. This guide covers both, plus
the CLI that drives migrations and the query interface used to introspect a live schema.
sync() is convenient for local development and tests. Migrations are what you want for
anything you have to reproduce or roll back deliberately — staging, production, or shared
databases.

prorm.sync()#

sync() walks every model registered with prorm.define(...) and makes the database
match. Its behaviour is controlled by SyncOptions:
import { Prorm, DataTypes } from 'ts-prorm-orm';

const prorm = new Prorm({
  dialect: 'sqlite',
  storage: ':memory:',
});

const User = prorm.define('User', {
  name: { type: DataTypes.STRING, allowNull: false },
  email: { type: DataTypes.STRING, unique: true },
});

// Default mode: create any missing tables, leave existing ones untouched.
await prorm.sync();
sync() returns the Prorm instance, so it is chainable, and it runs beforeSync /
afterSync hooks (globally and per model) unless you pass hooks: false.

Modes#

The three modes are mutually exclusive — force wins over alter, and with neither set
you get the default "create missing tables only" behaviour.
// Default: only CREATE TABLE for tables that don't exist yet. Never drops data.
await prorm.sync();

// force: DROP every table (in reverse order, CASCADE) and recreate from scratch.
// Destroys all data — great for tests, never for production.
await prorm.sync({ force: true });

// alter: keep existing tables and data, but add/remove/modify columns to match models.
await prorm.sync({ alter: true });
[!CAUTION]
sync({ force: true }) drops every model's table, with CASCADE, before
recreating it. Pointed at the wrong DATABASE_URL it empties production in one
call. Guard it with match (below) and never run it from application start-up.
alter also accepts an object to control what happens to columns that are absent
from the model. alter: true never drops: it logs a warning naming each unknown
column and leaves it in place (it used to mean drop, which is why the object form
exists). Dropping is opt-in:
// The same as alter: true — add and change columns, warn about unknown ones.
await prorm.sync({ alter: { drop: false } });

// Really drop columns that are no longer in the model.
await prorm.sync({ alter: { drop: true } });
A primary-key column is never dropped by sync({ alter }) whatever drop says.

Soft-dropping columns (4.9.2+)#

alter: { drop: 'soft' } keeps the data while getting the column out of the
model's way. Each unknown column is renamed to _prorm_dropped_<col>_<YYYYMMDD>,
made nullable so inserts keep working, and recorded in a prorm_dropped_columns
table. Three helpers manage what was parked there; alter: { drop: 'hard' }
(or true) really drops.
await prorm.sync({ alter: { drop: 'soft' } });

const parked = await prorm.listSoftDroppedColumns('users');
// [{ table: 'users', archivedName: '_prorm_dropped_nickname_20260926',
//    originalName: 'nickname', droppedAt: Date, dialect: 'sqlite' }]

await prorm.restoreSoftDroppedColumn('users', 'nickname'); // rename it back
await prorm.purgeSoftDroppedColumns('users');              // DROP the parked columns for good
The data really is still there. After the soft drop above, a row written before
it reads back with its value under the archived name:
{ id: 1, email: 'a@x.test', _prorm_dropped_nickname_20260926: 'ace', createdAt: …, updatedAt: … }
A parked column has three possible futures, and only one of them loses data:
A retention window is the usual way to purge: keep everything parked for a
month, then clear it out from a scheduled job.
// Drop only what was parked more than 30 days ago; returns what it dropped.
const purged = await prorm.purgeSoftDroppedColumns(undefined, { olderThan: '30d' });
for (const c of purged) console.log(`dropped ${c.table}.${c.archivedName}`);
[!WARNING]
restoreSoftDroppedColumn() refuses when the table has a column with the
original name again — a model that re-declared nickname and synced has
created a new, empty one. Rename or drop that column first, or you would have
to choose which of the two to lose.
The flowchart shows the branch sync({ alter }) takes for a column the model
no longer declares, and that only the 'hard' branch (and purge) destroys
data. The soft-drop branch and its three helpers are being added in 4.8.3 and
are not in the 4.8.2 package; the alter: true warn-and-keep behaviour is.

Other SyncOptions#

OptionTypeDefaultPurpose
forcebooleanfalseDrop and recreate all tables.
alterboolean | { drop?: boolean | 'soft' | 'hard' }falseReconcile columns in place, preserving data. true warns about unknown columns and keeps them; 'soft' / 'hard' are 4.9.2+.
matchRegExp–Only sync if the configured database name matches.
loggingboolean | (sql, timing?) => voidfalseLog the SQL that sync emits.
hooksbooleantrueRun beforeSync / afterSync hooks.
indexesbooleantrueAlso sync indexes.
constraintsbooleantrueAlso sync constraints.
match is a safety valve: sync silently no-ops unless the database name matches the
pattern, which is handy for guarding destructive force runs.
// Only ever wipe databases whose name contains "test".
await prorm.sync({ force: true, match: /test/ });

Migrations#

A migration is a module that exports two async functions, up and down. Each receives
a QueryInterface (the schema-manipulation API) and the Prorm instance:
// migrations/20240315120000-create-users-table.js
module.exports = {
  up: async (queryInterface, prorm) => {
    await queryInterface.createTable('users', {
      id: { type: 'INTEGER', primaryKey: true, autoIncrement: true },
      name: { type: 'VARCHAR(255)', allowNull: false },
      email: { type: 'VARCHAR(255)', allowNull: false },
      createdAt: { type: 'DATETIME', allowNull: false },
    });
    await queryInterface.addIndex('users', 'users_email_idx', ['email'], { unique: true });
  },

  down: async (queryInterface, prorm) => {
    await queryInterface.dropTable('users');
  },
};
down should undo exactly what up did, in reverse. If a migration has no meaningful
rollback you can omit down, but then reverting it only removes the tracking row without
touching the schema.

File naming and ordering#

Migration files are named <timestamp>-<name> where the timestamp is a 14-digit
YYYYMMDDHHMMSS value, e.g. 20240315120000-create-users-table.js. Migrations are run in
ascending order of that name (via localeCompare), so the timestamp prefix is what
guarantees correct ordering — not filesystem order. Reverts run in the opposite order.
.js and .ts migrations are both supported. TypeScript files are loaded through
ts-node, which is registered lazily the first time a .ts migration is loaded; if
ts-node is not installed you'll get a clear error telling you to install it or use .js.
A typed migration exports up and down directly; the loader accepts
module.exports = { up, down }, named exports and a default export alike.
import { DataTypes, type Prorm, type QueryInterface } from 'ts-prorm-orm';

export async function up(queryInterface: QueryInterface, prorm: Prorm): Promise<void> {
  await queryInterface.createTable('orders', {
    id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
    userId: {
      type: DataTypes.INTEGER,
      allowNull: false,
      references: { model: 'users', key: 'id', onDelete: 'CASCADE' },
    },
    totalCents: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 },
    placedAt: { type: DataTypes.DATE, allowNull: false },
  });
  await queryInterface.addIndex('orders', 'orders_user_placed_idx', ['userId', 'placedAt']);
}

export async function down(queryInterface: QueryInterface): Promise<void> {
  await queryInterface.dropTable('orders');
}
A project with a handful of migrations looks like this. The timestamp prefix is
the whole ordering contract, so never rename a file that has already run
anywhere — its name is the key in the tracking table.
my-app/
  .env                                       # DATABASE_URL for the CLI
  config/
    config.ts                                # scaffolded by prorm init
  migrations/
    20260926175708-create-users-table.js
    20260926175719-add-users-display-name.js
    20260926120500-create-orders.ts          # sorts first: its timestamp is earlier
  models/
    index.ts
  seeders/
    1790460065308-roles.js

A migration from start to finish#

This walks one change — a users table, then a column added to it — from an
empty directory to production and back. Every output block is what the CLI
prints.

Point the CLI at a database#

The CLI reads DATABASE_URL first, then DB_DIALECT with DB_NAME, DB_HOST,
DB_PORT, DB_USER and DB_PASSWORD. With nothing set it opens a SQLite file
called database.db in the current directory.
DATABASE_URL=postgres://app:secret@localhost:5432/app_dev
DATABASE_URL=mysql://app:secret@localhost:3306/app_dev
DB_DIALECT=sqlite
DB_NAME=./app.db
The CLI does not load .env by itself: export the variables, or run it through
something that does (node --env-file=.env, dotenv -e .env --, your process
manager).

Create the migration#

$ npx prorm migration:create -- --name create-users-table
Created migrations directory: /srv/my-app/migrations
Migration created: /srv/my-app/migrations/20260926175708-create-users-table.js
Migration created successfully: 20260926175708-create-users-table.js
The file is a CommonJS template with an empty up and down. Fill both in:

Add a second change#

A later change is a new file, never an edit to one that has already run:
$ npx prorm migration:create -- --name add-users-display-name
Migration created: /srv/my-app/migrations/20260926175719-add-users-display-name.js
Migration created successfully: 20260926175719-add-users-display-name.js
The new column is nullable on purpose: adding a NOT NULL column with no
default fails on any table that already has rows.

Check what will run#

$ npx prorm migration:status
Database connection established

Migration Status:
================

Pending Migrations:
  [DOWN] 20260926175708-create-users-table
  [DOWN] 20260926175719-add-users-display-name

----------------
Total: 2 (0 executed, 2 pending)

Run it#

--verbose lists what ran; without it you get the count.
$ npx prorm migration:run --verbose
Database connection established
Running 2 pending migration(s)...
Running migration: 20260926175708-create-users-table
Migration 20260926175708-create-users-table completed
Running migration: 20260926175719-add-users-display-name
Migration 20260926175719-add-users-display-name completed
Executed 2 migration(s)
  - 20260926175708-create-users-table
  - 20260926175719-add-users-display-name
On SQLite the database now holds exactly this:

Deploy#

In CI or on the release host, the same command runs against production. Run it
once, before the new application code starts, and stop the deploy if it fails:
$ DATABASE_URL="$PROD_DATABASE_URL" npx prorm migration:run
Database connection established
Running 2 pending migration(s)...
Running migration: 20260926175708-create-users-table
Migration 20260926175708-create-users-table completed
Running migration: 20260926175719-add-users-display-name
Migration 20260926175719-add-users-display-name completed
Executed 2 migration(s)
Running it again is safe — nothing is pending, so nothing runs:
$ npx prorm migration:run
Database connection established
No pending migrations to run

Roll back#

migration:revert with no flags undoes the most recent migration:
$ npx prorm migration:revert
Database connection established
Reverting last migration...
Reverting migration: 20260926175719-add-users-display-name
Migration 20260926175719-add-users-display-name reverted
Reverted 1 migration(s)

$ npx prorm migration:status
Database connection established

Migration Status:
================

Executed Migrations:
  [UP] 20260926175708-create-users-table [Batch 1]

Pending Migrations:
  [DOWN] 20260926175719-add-users-display-name

----------------
Total: 2 (1 executed, 1 pending)
--steps goes further back, newest first:
$ npx prorm migration:revert -- --steps 2 --verbose
Database connection established
Reverting 2 migration(s)...
Reverting migration: 20260926175719-add-users-display-name
Migration 20260926175719-add-users-display-name reverted
Reverting migration: 20260926175708-create-users-table
Migration 20260926175708-create-users-table reverted
Reverted 2 migration(s)
  - 20260926175719-add-users-display-name
  - 20260926175708-create-users-table
[!CAUTION]
Reverting add-users-display-name runs its down(), which drops the column
and every value in it. A rollback is only as safe as the down() it runs:
read it before you revert in production, and take a backup first.

The lifecycle of one migration#

When a migration fails half-way#

Where DDL is transactional (PostgreSQL and its family, SQLite, SQL Server,
Db2), the migrator runs each migration's up() (or down()) and its
PrormMeta row in one transaction. A failure part-way rolls back the
statements that already ran, and the migration stays pending. On SQLite, with
an index on a column that does not exist:
$ npx prorm migration:run
Database connection established
Running 1 pending migration(s)...
Running migration: 20260926120000-create-orders
Migration 20260926120000-create-orders failed: SqliteError: no such column: "customerId" - should this be a string literal in single-quotes?
Error: no such column: "customerId" - should this be a string literal in single-quotes?
The orders table that up() created first is gone again, and PrormMeta has
no row for the migration. Fix the file and run again.
Some statements cannot run inside a transaction: CREATE INDEX CONCURRENTLY,
and ALTER TYPE … ADD VALUE before PostgreSQL 12. Opt such a migration out in
its file, or turn transactions off for a whole run:
// In the migration file:
export const transaction = false;

// Or for every migration the migrator runs:
const migrator = new Migrator({ prorm, path: './migrations', transaction: false });
[!IMPORTANT]
MySQL, MariaDB and Oracle commit every DDL statement implicitly, so there the
migrator cannot roll a half-applied migration back, and neither can an
opted-out migration anywhere. Keep one logical change per migration and make
each statement safe to repeat (createTable emits CREATE TABLE IF NOT EXISTS; an addColumn fails the second time, and you remove the
half-applied change by hand first).
[!NOTE]
Before 4.9.2 the migrator never opened a transaction, on any engine: a
failure left the earlier statements applied. A migration that relies on a
statement PostgreSQL refuses inside a transaction (CREATE INDEX CONCURRENTLY) now fails until you add export const transaction = false.

Changing a column without downtime#

A migration that rewrites a column in one step locks the table and gives you no
way back. Splitting it into three leaves a working system between each pair.
Only the last step destroys anything, which is why it is last and separate. Stop
after step two and you have a working system on the new column with the old one
still there as a fallback — see Migration rollback.

Migrations that are not CREATE TABLE#

Most guidance covers tables and columns. These are the ones that come up next,
and each has a detail that is easy to get wrong by hand.
The queryInterface a migration is handed covers tables, columns, indexes,
constraints, partitions and bulk data. The catalog operations below — enums,
extensions, sequences, statistics, comments — live on the full query interface,
which the prorm argument hands you:
import type { CreateQueryInterface, Prorm, QueryInterface } from 'ts-prorm-orm';

export async function up(queryInterface: QueryInterface, prorm: Prorm) {
  const qi: CreateQueryInterface = prorm.getQueryInterface();
  await qi.addEnumValue('order_status', 'refunded', { after: 'delivered' });
}
The examples below assume that qi.

Adding a value to an enum#

await qi.addEnumValue('order_status', 'refunded', { after: 'delivered', ifNotExists: true });
The only way to extend a live enum. Recreating the type would mean dropping
every column that uses it. ifNotExists makes the migration safe to re-run
after a half-way failure.
Warning: PostgreSQL cannot run ALTER TYPE … ADD VALUE inside a transaction
block on versions before 12, and the migrator runs each migration in one. On
those versions, put export const transaction = false; in the migration.

Installing an extension#

await qi.createExtension('uuid-ossp');
await qi.createExtension('pg_trgm');
Run this before any migration that uses uuid_generate_v4() as a column
default, and remember that installing an extension usually needs more privilege
than the application's own role has.

Resetting a sequence after a backfill#

await qi.bulkInsert('users', rowsWithExplicitIds);
// Without this, the next ordinary insert collides on the primary key.
await qi.setSequenceValue('users_id_seq', 10_000);
This is the classic one. Rows inserted with explicit ids leave the sequence
behind them, and nothing fails until an ordinary insert happens — often hours
later, in a different code path, looking like corruption.

Refreshing statistics after a large change#

await qi.addIndex('events', ['occurred_at']);
await qi.analyze('events');
A planner working from statistics gathered before a bulk load is planning for
the table as it was. Worth a line at the end of any migration that moves a lot
of rows.

Documenting the schema as you change it#

await qi.commentOnTable('events', 'Append-only; see the retention job');
await qi.commentOnColumn('events', 'payload', 'Raw webhook body, unvalidated');
Comments live in the catalog, so they survive a restore and show up in every
client — a better home for this than a wiki page nobody updates.

Making a migration reversible#

Each of the above has an inverse, and down() is where most of them are
forgotten:
up()down()
addEnumValue()No inverse. PostgreSQL cannot remove an enum value; recreating the type is the only route.
createExtension()dropExtension() — but only if nothing else came to depend on it.
createDomain()dropDomain()
commentOnTable(t, 'x')commentOnTable(t, null)
setSequenceValue()Record the old value first; there is nothing to read it back from afterwards.
Note: an irreversible up() is not a reason to skip down(). Throw an error
explaining why, so a rollback fails loudly at the migration that cannot be
undone rather than half-way through the one after it.
Because the error is thrown before the tracking row is removed, the migration
stays [UP] and every migration older than it stays applied.

Running migrations programmatically#

Migrator discovers migration files, tracks which have run, and runs or reverts them.
import { Prorm } from 'ts-prorm-orm';
import { Migrator } from 'ts-prorm-orm'; // also: createMigrator(options)

const prorm = new Prorm({ dialect: 'sqlite', storage: './app.db' });
await prorm.connect();

const migrator = new Migrator({
  prorm,
  path: './migrations',   // default './migrations'
  tableName: 'PrormMeta', // tracking table, default 'PrormMeta'
});

// Discover files on disk before inspecting/running.
await migrator.findMigrations();

// Run every pending migration.
const result = await migrator.up();
console.log(`Ran ${result.executed}:`, result.migrations);

// Run just one, by name.
await migrator.up({ migration: '20240315120000-create-users-table' });
Migrator also loads files automatically the first time you call up() / down() if
none have been registered yet, so findMigrations() is only strictly required when you
want to inspect state (pending(), executed(), status()) beforehand.

Reverting#

// Revert the most recently executed migration.
await migrator.down();

// Revert the last N migrations (reverse order).
await migrator.down({ steps: 3 });

// Revert one specific migration.
await migrator.down({ migration: '20240315120000-create-users-table' });

// Revert every migration in a batch above the target batch number.
await migrator.downToBatch(2);
Each of these returns a MigratorResult — { executed, reverted, migrations }.

Inspecting state#

const pending = await migrator.pending();   // Migration[] not yet run
const done    = await migrator.executed();  // Migration[] already run
const status  = await migrator.status();    // MigrationStatus[]

for (const s of status) {
  console.log(s.name, s.status, s.batch); // status: 'up' | 'pending' | 'down'
}

Migration storage & tracking#

Applied migrations are recorded in a tracking table so the migrator knows what has already
run. By default it is called PrormMeta; override it with MigratorOptions.tableName (and
optionally schema). PrormMigration.ensureMigrationsTable() creates it with:
name — the migration name (unique)
batch — an integer grouping migrations
timestamp — unix seconds when it was applied
Migrator.up() — and so prorm migration:run — records every migration of
one run under one batch
, one higher than the last run's. A run that applies
two migrations leaves both in batch 1, so downToBatch() undoes a whole deploy.
migration:revert with no flags still undoes only the newest migration. After
the walkthrough above, the table reads:
name                                    batch  timestamp
20260926175708-create-users-table       1      1790485935
20260926175719-add-users-display-name   1      1790485935
Before 4.9.2 each migration got a batch of its own (1 and 2 above).
downToBatch(target) reverts every migration whose batch is above target,
newest first, so it is the programmatic way back to a known point:
const before = await migrator.status();
const lastGood = Math.max(0, ...before.filter((s) => s.status === 'up').map((s) => s.batch ?? 0));

const result = await migrator.up();
try {
  await runSmokeTests();
} catch (err) {
  // Undo exactly what this deploy applied, then fail the deploy.
  await migrator.downToBatch(lastGood);
  throw err;
}
You never touch this table directly; the migrator manages it. MigratorOptions also
accepts extension (default .js, .ts always allowed) and a custom filter(filename)
to control which files are treated as migrations.

CLI#

The prorm binary (also installed as orm-cli, the same file) wraps the same
Migrator. It reads its connection from DATABASE_URL, or from DB_DIALECT,
DB_NAME, DB_USER, DB_PASSWORD, DB_HOST and DB_PORT — only from the
environment; the config/config.ts that prorm init writes is for your
application, not for the CLI.
$ npx prorm init
$ npx prorm migration:create -- --name create-users-table
$ npx prorm migration:run
$ npx prorm migration:run -- --name 20240315120000-create-users-table
$ npx prorm migration:revert
$ npx prorm migration:revert -- --steps 3
$ npx prorm migration:revert -- --name 20240315120000-create-users-table
$ npx prorm migration:status
$ npx prorm migration:run -- --path ./db/migrations
FlagUsed byDoes
--name <name>migration:create, migration:run, migration:revertThe new file's name, or the one migration to run or revert.
--steps <n>migration:revertRevert the last n, newest first.
--path <dir>allMigrations directory; default ./migrations.
--verbosemigration:run, migration:revertList each migration after the count.
--quiet, -qallPrint errors only.
--no-color / NO_COLORallPlain output for logs.
The -- separator matters under npm run and npx: flags after it go to the
command rather than to npm. Run npx prorm --help for the full reference. Newly
created migrations use a CommonJS template (module.exports = { up, down }) with
a .js extension.

In CI#

A deploy job that migrates before the new code goes live, and fails the deploy
if a migration fails:
The CLI exits non-zero when a migration throws, so needs: migrate keeps the
new code from starting against a schema it does not match.

Schema introspection with QueryInterface#

The same QueryInterface passed into migrations is a full schema-manipulation and
introspection API. Inside a migration you already have it; standalone you can reach it via
migrator.getQueryInterface().
const qi = migrator.getQueryInterface();

// Introspection
const tables = await qi.showTables();                 // string[]
const exists = await qi.tableExists('users');         // boolean
const hasCol = await qi.columnExists('users', 'email');
const columns = await qi.describeTable('users');      // { [col]: ColumnDescription }
const indexes = await qi.showIndexes('users');        // IndexDescription[]
const fks     = await qi.getForeignKeysForTable('users');
const version = await qi.getDatabaseVersion();
Schema-changing operations available on QueryInterface include:
CategoryMethods
TablescreateTable, dropTable, dropAllTables, renameTable
ColumnsaddColumn, removeColumn, changeColumn, renameColumn
IndexesaddIndex, removeIndex
ConstraintsaddConstraint, removeConstraint
Foreign keysaddForeignKey, removeForeignKey
PartitionscreatePartitionedTable, createPartition, attachPartition, detachPartition, dropPartition
DatabulkInsert, bulkUpdate, bulkDelete
RawprormQuery(sql, options)
// A data-migration example using the bulk + raw helpers.
export const up = async (queryInterface, prorm) => {
  await queryInterface.addColumn('users', 'status', {
    type: 'VARCHAR(20)',
    allowNull: false,
    defaultValue: 'active',
  });

  await queryInterface.bulkUpdate('users', { status: 'legacy' }, { createdAt: null });

  const { rows } = await queryInterface.prormQuery('SELECT COUNT(*) AS n FROM users');
  console.log('backfilled', rows[0].n, 'rows');
};
Note that QueryInterface's constraint support is intentionally narrow: addConstraint
only materialises unique constraints (as a unique index); primaryKey, foreignKey, and
check are best expressed at table-creation time or via addForeignKey / raw SQL.

Choosing sync vs migrations#

Use prorm.sync({ force: true }) in tests and throwaway local databases — fast, no files
to maintain, but destructive.
Use prorm.sync({ alter: true }) for quick iteration on a dev database you want to keep.
Use migrations for anything shared or long-lived: they are versioned, ordered,
reversible, and tracked in PrormMeta, so every environment can be brought to the same
schema state deterministically.

Next: Indexes & constraints — making those queries fast and those invariants enforced.

Related reading#

Schema diffing — see how the live schema differs from your models
Query interface — DDL outside a migration
Seeders — the same machinery, for data rather than shape
Modified at 2026-10-04 00:40:16
Previous
Migrate — the data transfer subsystem
Next
Models from an existing database
Built with