Skip to content

Admin guide

Configure a read-only Laravel database connection to the Voyager database. Run php artisan voyager-importer:inventory --connection=voyager --json to inspect the schema. Inventory never reads setting values.

Run php artisan voyager-importer:import --connection=voyager --only=users,roles,settings --json to preview. Execution requires --execute, --only, --source-label and interactive confirmation. Unattended execution and the production, prod and live environments require --force. Execution creates a Migration Assistant session and reports created, updated, skipped and conflicting rows. The source label identifies provenance; the manifest also fingerprints the server identity and schema and execution refuses changes during a session. A failed execution prints the exception class and, for a database error, its SQLSTATE and driver error code, never row values or message text from the database, and stores that on the session. Only the importer’s own fixed-text refusals show their message. Voyager permissions rows that repeat a key are imported once and counted as skipped duplicates. A user avatar is reported as unmapped when the destination users table has no avatar column.

Choose a stable, unique, short lowercase slug for each source installation, such as customer-a-2026. Reuse it for reruns and resumes of the same source, and use a different label when restoring another source into the same scratch database. Hostname case and default MySQL/PostgreSQL ports are normalised; localhost, 127.0.0.1 and ::1 are not treated as equivalent, because they can select different network or socket endpoints. Keep the connection configuration consistent as well as the label.

The source-label registry binds each label to the database server behind the connection (its server-reported identity: the server UUID on MySQL, server id and port on MariaDB (the host name is not used, because in a container it changes whenever the container is recreated), the system identifier on PostgreSQL, and the file on SQLite), not to its configuration. The label also records the connection’s host, port and database, so moving it to another host or database name, even one with the same server id and port, requires --repoint-source. Two identical compose stacks on the same host, port and database name are indistinguishable (a MariaDB server id and port can match), so give each source a distinct label and never reuse a label across them. Pointing a label at a different server, even through unchanged connection settings, requires --repoint-source; binding a server already registered to another label requires --new-source. These decisions are recorded in the session. A prepared session is refused at execution, including when resumed or queued, if its label has since been repointed to another server. The importer also compares server-reported identities to reject aliases of the destination database. PostgreSQL identifies a server through pg_control_system(), which only a superuser may call by default. A role without that privilege is refused with the missing privilege named; as a superuser run GRANT EXECUTE ON FUNCTION pg_control_system() TO <role> or connect with a role that has it. A connection whose identity cannot be verified is refused. Apply the Migration Assistant migrations before using this version.

Earlier alias-based provenance cannot establish which database produced a row. Execution stops for legacy Voyager sessions or keys in the selected scope. Do not delete mappings or reuse them for a different source without verifying their origin. Reconcile them instead with php artisan voyager-importer:reconcile-legacy --connection=voyager --source-label=customer-a-2026 [--alias=voyager] [--site-id=1]. Legacy keys were built from the connection alias, so --alias names the alias that was used then (it defaults to --connection) and --source-label the label of the database that really produced the rows. Without --execute the command is a dry run that reports how many keys can be re-keyed. Each legacy key is matched by hash against the keys the source rows would have had under that alias, so the alias is only trusted as far as the rows confirm it. Matched keys are re-keyed to the label and legacy sessions in the scope are stamped with it; the whole change is rolled back if any legacy provenance would remain. Keys that no source row reproduces (for example rows deleted from the source) leave execution refused until you correct --alias or pass --forget-unmatched, which drops them: a dropped key lets that row be imported again, so it can create a duplicate when the destination row has no natural key. Use --site-id to reconcile a site’s scope and omit it for the global scope. --force skips the confirmation.

A preview with --site-id also lists navigation_conflicts: source menus whose key is already held in the destination site by a live or a soft-deleted navigation, which execution refuses. A preview or an execution whose selected source tables are all missing or empty reports that there is nothing to import, and execution refuses before creating a session. Rolling back an import removes the rows it created outright, including soft-deleting models, so the same source can be imported again, and removes the role assignments it created (and only those; with permission teams enabled each is journalled under its team, and an unknown team key refuses the journal). A row is purged only when nothing added after the import references it; otherwise it is soft-deleted and the reason is recorded under soft_deleted in the rollback summary. Role assignments carry no edit guard: one an administrator re-created or changed after the import is still removed by the rollback, because a pivot row has no timestamps to compare. The URL, slug and translated-slug lists in conflict reports are shown in full: slugs are public routes, not customer data. After an import the target site’s cached URLs are marked stale when the HTML cache package is installed.

Translations are saved quietly to prevent Core from creating untracked automatic URLs. The importer refreshes Core’s caches and content graph, but host listeners on the Eloquent translation save event will not run. Activity-log entries are not written for quietly saved translations or users; review integrations that depend on those events before execution. The import summary warns when such rows were saved; unchanged reruns do not produce that warning.

The import command accepts:

  • --connection: a configured source connection. It is mandatory for execution and must not be Laravel’s default connection name.
  • --source-label: required for execution and resume; a lowercase alphanumeric slug with hyphen separators, at most 64 characters. The label identifies the source, not the scratch database.
  • --repoint-source: explicitly bind an existing label to a changed connection tuple. Label rows registered before the tuple check (this package is unreleased) carry no stored tuple, so run --repoint-source once to record it.
  • --new-source: explicitly bind a new label to a tuple already used by another label.
  • --only: comma-separated entities. Supported names are users, roles, settings, media, pages, posts, translations, menus, redirects and tables; the importer orders them by dependency.
  • --site-id: destination site ID, required for site-owned entities.
  • --actor-id: user ID recorded on the session. Rollback also requires an authorised actor.
  • --locale-map: comma-separated source-to-language-code pairs, such as fr_FR=fr.
  • --role-map: comma-separated source-to-target role names, such as admin=voyager-admin. Source roles matching configured Capell roles, including admin and super_admin, are refused unless mapped to a non-privileged target; preview lists refused names.
  • --post-url-prefix: the source site’s post route prefix, such as /articles (use / for root). Required when importing posts with slugs so their source URLs can be preserved; confirm it against the source site’s routes.
  • --page-layout, --page-blueprint, --article-layout, --article-blueprint: the layout and blueprint keys imported pages and articles are created with, for a target theme that uses other keys (for example capell-marketing-layout). Each is optional and falls back to the site’s default page layout and blueprint, or the article key for articles. A preview with --site-id lists missing ones as missing_prerequisites, and execution refuses before writing anything.
  • --force: skip confirmation; mandatory in production and when running without interaction.
  • --publish-from-source-status: retain supported publication states; otherwise imported pages and posts are drafts.
  • --skip-unmapped: import supported page and post fields while recording the identities of skipped fields in the session result. Field values are not reported. Without this option, populated unmapped fields fail the import.
  • --session: resume a validated or failed session by UUID with its saved options. Supply the same --connection and --source-label; do not combine it with changed mapping options.
  • --json: print the session result as JSON. Without it, execution prints the session UUID.

Execution is not yet a complete Voyager migration. Global entities and site-owned entities cannot currently be selected in the same execution. Permission groups and category relationships are not imported. Media replacement is refused; source files must be on the local public disk, and supported binaries are copied to the configured local Media disk. Page images remain unmapped unless explicitly skipped. Contributor tables require explicit import definitions; see the extension contracts. Unrecognised translation fields and unmapped locales fail. Review the result and rollback report before proceeding.

The media preview counts distinct paths in supported source fields and mapped JSON columns. It does not check whether files exist. Redirects are counted from a source redirects table when present. See the synthetic workbench for a reproducible preview.

  • Unknown source label, or the source server has changed: a label is bound to the database server behind the connection. Reuse the original label. If the source has genuinely moved, rerun with --repoint-source; to bind a server already registered to another label, use --new-source.
  • Duplicate permission keys: Voyager permissions rows that repeat a key are imported once and reported as skipped duplicates. No action is needed.
  • A column such as avatar is reported as unmapped: the destination table does not have that column. Add it, or pass --skip-unmapped to record the skipped field names.
  • Execution fails: read the Cause: line. It names the exception class and, for a database error, the SQLSTATE and driver error code, never row values, and is stored on the session. Fix the cause, then resume with --session=<uuid>, the same --connection and the same --source-label.

Running with --only=roles and without users assigns each imported role to the users that an earlier session already imported from the same source (found through the source-key table, using the users’ Voyager role_id). A source user that was not imported earlier is listed as an unmapped users:<id>:role_id (user not imported) field rather than assigned.