# Migration

Imports accounts over SSH from cPanel or DirectAdmin, the two sources measured end to end. The wizard also detects Plesk, HestiaCP, CyberPanel, CloudPanel and CWP on the source server, and it can import from another WHost server through that server's API (no SSH); those sources have not been measured yet and migration is experimental in the beta (see *Known Limitations*). An import from another WHost server writes the account's `public_html` (replaced) and `mail` (merged) through directory handles as the account's user — also when the account already exists here: a link the account placed at `public_html` is replaced by a plain folder, a link at `mail` makes that account's import fail rather than being written through. The panel follows each job by polling its status; the API also offers the progress as an SSE stream (`GET /migration/status/{job_id}/stream`).

| Step | Action |
|---|---|
| 1 | Source server credentials (SSH host, port, root/sudo user, password or key) |
| 2 | Scan — agent pulls the account list and lets the operator pick which to migrate (filter by domain pattern) |
| 3 | Start — per account, in this order: create the account here, copy the files (`.htaccess` rules checked, LSCache detected), the databases and the mailboxes (each can be skipped), then cron jobs and FTP accounts. SSL certificates are not carried over — issue them again here (see the FAQ) |
| 4 | Follow the job until every account is finished or failed |
| 5 | Cut-over — check the sites and mail here, then point the domains' DNS at this server; no DNS records are exported or compared for you |

Failed accounts can be retried while the scan's SSH credentials are still in memory; accounts that finished are not run again. An agent restart does not resume a running job: it is marked failed ("Server restarted during migration") and the credentials are gone, so scan the source server again before retrying (`400 MIGRATION_CREDENTIALS_EXPIRED` otherwise). SSH credentials are AES-encrypted in memory and zeroed after the job completes.

**Boundaries the transfer keeps.** The source server's SSH host key is pinned on first use (`/var/lib/whost/migration_known_hosts`); a later mismatch aborts the connection with the fingerprint in the message. Files and mailboxes are unpacked on this server **as the migrated account's own user**, so nothing the source archive contains can reach outside what that account may already write. Only databases named inside the account's own prefix (`<username>_*`) are created locally; a source that reports any other name is skipped with a warning. Their dumps are imported the way a backup restore imports them — by the account's import user, which can write to the account's own databases only (see *Backups — scope and settings*): a dump that names another database is refused at that statement and the agent log names the database. The private/loopback/link-local address ranges are refused as a source host.

**Restoring from an uploaded backup file is not available.** Migrate over SSH with the steps above. The panel has no Upload tab, and `POST /migration/upload` and `POST /migration/upload/restore` answer `409 MIGRATION_UPLOAD_RESTORE_UNAVAILABLE` without keeping the file.

**API endpoints:** `POST /migration/scan`, `GET /migration/scan/{job_id}`, `POST /migration/start`, `GET /migration/status/{job_id}`, `GET /migration/status/{job_id}/stream` (SSE), `POST /migration/cancel/{job_id}`, `POST /migration/retry/{job_id}`, `GET /migration/history`, `GET /migration/history/{job_id}`.

---
