Migrations
Changing the database schema with the embedded migration CLI.
SQL migrations live in apps/server/migrations as numbered up and down
files. They are embedded in the binaries, so a deployed image carries its own
schema.
Commands
pnpm migrate:up # apply what is pending
pnpm --filter @montara-pass/server run migrate:up:seed # up, then run the seeders
pnpm --filter @montara-pass/server run migrate:create add_x # new up/down pair
pnpm --filter @montara-pass/server run migrate:version # current version
pnpm --filter @montara-pass/server run migrate:force 2 # after a failed migration
pnpm migrate:down # roll back everything
pnpm --filter @montara-pass/server run migrate:refresh # down, then upThe CLI reads DB_DSN from the environment or apps/server/.env, or takes
--db-dsn.
down and refresh delete all data
down rolls back every migration, users included. Both commands refuse
a database that is not on localhost unless --allow-remote is passed.
On boot
With MIGRATE_ON_BOOT=true the API applies pending migrations before it
starts serving. With SEED_ON_BOOT=true it also runs the seeders, which are
idempotent: the categories, and the SUPER_USER account.
Existing migrations
| Version | What it creates |
|---|---|
000001 | categories |
000002 | users, mirroring authula.users |
000003 | the role column on users |
000004 | the vault: user_keys, folders, items, item_members |
000005 | the last_used_at column on item_members |
Cloud Edition migrations
apps/server/migrations/ee holds the tables only the Cloud Edition has.
They keep their own version in schema_migrations_ee, run after the main ones
and roll back before them. A Community Edition build never applies them, so
its database has none of these tables; a database that moves to the
Cloud Edition gets them on its next up or boot.
up, down, refresh and version act on both tracks. force and create
act on the main one unless --ee is passed:
go run ./cmd/migrate --ee create add_x # new pair in migrations/ee
go run ./cmd/migrate --ee force 1 # after a failed EE migration| Version | What it creates |
|---|---|
000001 | billing: plans, subscriptions, purchases |
Only an account with the user role holds a subscription or a purchase. The
database enforces it: both tables reference users (id, role) with the role
fixed to user, so a super user cannot subscribe, and a subscriber cannot be
given another role. Each table also takes only its kind of plan (recurring
or one_time), and a user has at most one live subscription.
Tests against a real database
apps/server/cmd/api has an integration test that drives the API end to end.
It is skipped unless TEST_DB_DSN points at a PostgreSQL 18 database it is
allowed to wipe.
With TEST_REDIS_URL as well, that test keeps its rate limit counts and
idempotency keys in Redis, and apps/server/internal/middlewares runs its
Redis tests: two replicas sharing one budget, one answer and one lock. They
are skipped without it. Point it at a database number nothing else uses, such
as redis://localhost:6379/15.