Docs
Systhema Design (opens in new tab)
Unreleased

systhema migrate

Run one data migration, or `--schema`.

On this page

Run a single data migration by id. These are the post-upgrade hooks that systhema upgrade --allow-database runs after pnpm install — the ones that rewrite stored database content rather than source files. systhema migrate exposes them individually, the same way systhema codemod exposes a single code codemod, for when you need to (re-)run one outside a full upgrade — e.g. after restoring a database snapshot, or to verify a migration in isolation.

systhema migrate --list                          # list available data migrations
systhema migrate unify-block-slugs-data          # run by id
systhema migrate unify-block-slugs-data --dry-run # plan only (reports it would run, runs nothing)

Each migration is idempotent and guards itself with an applicability check, so running one against an already-migrated project, the wrong project type, or a fresh install is a clean no-op. Under the hood each one is a thin wrapper around a project-local Payload bin script (with the package manager substituted in), so the two forms below are equivalent — systhema migrate just adds the --list index, the applicability check, and --dry-run:

systhema migrate unify-block-slugs-data
# ≡ pnpm payload migrate-unify-block-slugs

Available data migrationsLink to this section

The currently bundled data migrations (Payload projects only):

IDReleaseKindWrites databaseDescription
migrate-emails-data1.6.0renameyesCopies the legacy emails global into general-settings.emails. Disabled tabs are skipped; an enabled tab requires its SQL destination. Preserve the legacy data until copied. Run nav renames before schema additions, then copy settings. For push-mode PostgreSQL, review systhema migrate --schema --dry-run, then apply systhema migrate --schema; do not run payload migrate with a dev-push marker or generate a full-schema migration without a baseline. For a migration-based project with an existing snapshot baseline and no dev marker, use pnpm payload migrate:create, review the DDL, then pnpm payload migrate. systhema migrate --schema needs @systhemaui/cli and @systhemaui/payload at 1.7.0-canary.16 or later; an older Payload package does not register the command.
migrate-header-nav-dbnames1.6.0renameyesRuns the Payload bin script that renames the default Header global's nav objects — tables, and on Postgres also constraints, indexes, enum types, and owned sequences — to the namespaced forms header_nav / header_sub_nav (+ versioned _header_nav_v / _header_sub_nav_v) that Payload 3.85.0 generates (the dbName change shipped in v1.6.0 #65). Handles every pre-upgrade state: the 1.5.x long names (header_navigation / header_navigation_sub_navigation + _v_version_* siblings), the earlier canary bare names (nav / sub_nav + _nav_v / _sub_nav_v), and the half-migrated state where a prior canary renamed the tables but left the enum_nav_* enums and _nav_v_id_seq sequences stale. Enum types are the ones that trigger Drizzle's interactive rename prompt. Runs on Postgres AND SQLite (the dbName becomes the table name verbatim on both), idempotent, and a no-op on MongoDB, fresh installs, already-migrated DBs, custom Headers, and projects that strip enums to varchar via beforeSchemaInit. Safe to skip with --skip migrate-header-nav-dbnames if you have already renamed by hand.
unify-block-slugs-data1.6.0renameyesRuns the Payload bin script that walks every Lexical-bearing collection (pages, components, …) and rewrites legacy block slugs to the unified forms (rootImage/regionImage/fragmentImage/nestedImage → image; same for video/youtube/googleMap/embed; formBlock/nestedFormBlock → form; nestedCard → card; nestedColumns → columns; nestedCarousel → carousel; richTextInStack/buttonInStack/chipInStack/iconInStack/avatarInStack → richText/button/chip/icon/avatar; imageInStack/nestedImageInStack → image). Also injects _tier on every block. Idempotent. Safe to skip with --skip unify-block-slugs-data if you have already migrated by hand.
migrate-localize-status1.7.0renameyesPer-locale publishing is now on by default for every project with locales, so _status moves from the shared column on pages, posts and components (and their _v version tables) into their _locales siblings. The script performs the whole transition in a transaction and writes the matching Payload migration file when the project has a migration directory. It is idempotent: a second run reports "already migrated" and exits 0. Skip it with --skip migrate-localize-status if you are opting out with localization: { localizeStatus: false }, or if you migrate on your own deploy schedule.
migrate-seo-data1.7.0renameyesCopies the legacy seo global into general-settings.seo. Disabled tabs are skipped; an enabled tab requires its SQL destination. Preserve the legacy data until copied. Run nav renames before schema additions, then copy settings. For push-mode PostgreSQL, review systhema migrate --schema --dry-run, then apply systhema migrate --schema; do not run payload migrate with a dev-push marker or generate a full-schema migration without a baseline. For a migration-based project with an existing snapshot baseline and no dev marker, use pnpm payload migrate:create, review the DDL, then pnpm payload migrate. systhema migrate --schema needs @systhemaui/cli and @systhemaui/payload at 1.7.0-canary.16 or later; an older Payload package does not register the command.
migrate-capability-strings1.7.1renameyesRewrites stored per-user capability strings after the emails and SEO capability renames. On PostgreSQL, add the replacement enum labels before this hook. Run this before an operator-owned enum recreate that removes the legacy labels.

Migration behaviorLink to this section

  • ----------------------------: ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  • migrate-localize-status: Moves _status out of the shared column on pages, posts and components (and their _v version tables) into their _locales siblings, so each locale carries its own publication state. Runs in one transaction; a rerun reports "already migrated" and exits 0. On a project that keeps a Payload migration directory it also writes the matching *_systhema_localize_status.ts migration and records it in payload-migrations, so the same move reaches production through payload migrate. See Per-locale publishing.

  • migrate-header-nav-dbnames: Postgres + SQLite. Renames the default Header global's navigation / subNavigation tables to their explicit dbName forms (header_nav / header_sub_nav / _header_nav_v / _header_sub_nav_v), plus the dependent constraints/indexes/enums/sequences on Postgres and the indexes on SQLite. No-op on MongoDB.

migrate-localize-status can perform the complete transition itself. Per-locale publishing is on by default for every project with locales configured, so systhema upgrade plans the migration whenever the project is a Payload project, has locales, and has not opted out with localization: { localizeStatus: false }. It is idempotent, so declining it costs nothing: --skip migrate-localize-status leaves it for later, and systhema migrate migrate-localize-status runs it whenever you are ready. systhema doctor's localize-status-schema check reports a project that never ran it.

A Payload project can also run pnpm payload systhema-localization-report at any time — a read-only dry run printing which fields the content localization field policy's policy: 'all' would move from shared to localized, and how many rows the back-fill would write per locale. It is not part of systhema upgrade; systhema doctor's localization-policy check points at it.

Schema reconciliationLink to this section

For additive PostgreSQL schema reconciliation, use systhema migrate --schema --dry-run to print the live-catalog SQL and systhema migrate --schema to print and apply it in one transaction. Both run with stdin closed. This path works without Payload migration history and retains legacy tables. It is separate from named data hooks and is not run automatically by upgrade. See Non-interactive schema migration for supported changes, refusal cases, and the required nav/schema/SEO ordering.

OptionsLink to this section

FlagDescriptionDefault
--dry-runPlan only, no changesfalse
--listList available data migrationsfalse
--schemaAdd missing PostgreSQL schema objects without prompts; prints SQL firstfalse

See Database migrations for what these steps do, why they run before Payload's first schema push, and how they differ from project-owned Payload migrations.