Introduction

Must-Have Backup is a modern backup and restore plugin for WordPress, built around three ideas that set it apart from traditional backup tools:

  • Time-machine restore — beyond full snapshots, the plugin continuously captures every file change and every database write. You can restore your site to any moment in time, not just to the last snapshot.
  • Content-addressed deduplication — files are split into chunks identified by their SHA-256 hash. Identical content is stored once, so repeat snapshots consume almost no additional space.
  • Restores that can't brick your site — every restore is staged alongside the live install first. The final swap is atomic and fully reversible until you explicitly confirm it.

Key Highlights

  • Full + incremental backups — weekly full snapshots with continuous file and database delta capture in between
  • 10+ storage destinations — Amazon S3, Backblaze B2, Wasabi, DigitalOcean Spaces, MinIO, Dropbox, Google Drive, OneDrive, WebDAV/Nextcloud, SFTP, FTP/FTPS, local folders
  • End-to-end encryption — Argon2id + XChaCha20-Poly1305; chunks are encrypted before upload
  • Disaster Recovery Kit — a standalone recovery.php that restores your site without booting WordPress at all
  • Migration bundle — move to a new host or domain with a self-deleting standalone installer
  • Self-verifying — automatic chunk integrity checks, restore drills, storage quota monitoring
  • Notifications — email, Slack and Discord alerts on success, failure, quota and integrity events
  • Full WP-CLI support — every operation scriptable from the command line

Requirements

  • WordPress 5.9 or higher
  • PHP 8.0 or higher (libsodium — bundled with PHP — is used for encryption)
  • MySQL 5.7+ or MariaDB 10.3+

Getting Started

Installation & Licensing

  1. Download the plugin ZIP from your account at musthaveplugins.com
  2. In WordPress, go to Plugins > Add New > Upload Plugin
  3. Select the ZIP, click Install Now, then Activate
  4. Navigate to the Backup menu to configure

On first visit you are redirected to musthaveplugins.com to connect your site. After completing the connect flow, you are redirected back and the plugin is activated.

Updates are delivered directly from musthaveplugins.com through the normal WordPress Plugins screen — when a new version is available, the familiar "update now" notice appears, no manual ZIP juggling needed.

Important: backups always keep running, even if your subscription lapses. An active subscription is required for restore staging and promotion, updates and support — but your data safety is never held hostage: confirming or rolling back an in-flight swap and the disaster Recovery Kit are never gated, and a licensing-server outage falls back to your last known status rather than blocking you.

Your First Snapshot

  1. Go to Backup > Overview
  2. Click Create snapshot in the page header
  3. The plugin scans your files, dumps your database tables, and stores everything in the local chunk store under a randomized, web-inaccessible folder (wp-content/mhbackup-<random>/)

The first snapshot takes the longest since every chunk is new. Thanks to deduplication, subsequent snapshots only store what changed — a second run right after the first is nearly instant.

Once a snapshot exists, it appears on the Timeline tab, and — if you have storage destinations configured — upload jobs are queued automatically.

Settings Overview

The admin interface is organized into tabs:

  • Overview — protection status, recent snapshots, Scan for changes, the Restore drill card and the License card (connection to musthaveplugins.com with Connect, Reconnect, Disconnect and Re-check — needed for restore and the one-click cloud connectors; backups keep running without it). The Create snapshot button sits in the page header.
  • Timeline — every snapshot with restore, migration and time-machine actions
  • Restore — activity feed with restore points, time-machine target, per-table/per-path exceptions, partial restore, promote/confirm/rollback
  • Storages — add and manage off-site destinations, upload queue status, and Local storage on this server (full copy or remote-only)
  • Manage — snapshot × destination matrix, retention (global fallback, per destination, local disk), pinning
  • Schedule — full snapshot cadence, file scan interval, incremental push interval, continuous DB capture
  • Data preferences — file/folder/extension and database-table exclusions, activity-feed noise, non-prefixed tables, plus uninstall behaviour
  • Encryption — password mode (with a downloadable recovery key) or envelope mode
  • Migrate — build a migration bundle for moving the site
  • Notifications — email, Slack and Discord alerts

Deactivation & Uninstall

Deactivating the plugin removes the db.php capture dropin and unschedules all cron events — nothing else is touched.

Uninstalling keeps all backup data by default: the storage folder, the custom tables and your settings all survive, so reinstalling later loses nothing. Only when On uninstall, delete EVERYTHING (backups, chunks, tables, keys, options) is explicitly ticked (Data preferences tab) does uninstall wipe the storage folder, the custom tables and every plugin option — and even then the wipe is skipped while a restore is still waiting for Confirm or Rollback.

Snapshots

How Snapshots Work

A full snapshot captures both your files and your database:

  1. File scan — walks your WordPress installation from ABSPATH, honoring all exclusions. Each file is split into chunks and stored in the chunk store.
  2. Database dump — every table (minus excluded ones) is dumped to a compressed per-table SQL file, which is itself chunked and stored.
  3. Manifest — a JSON document recording the environment, the complete file list with chunk references, and the table list. The manifest is the single source of truth for restoring the snapshot.

Snapshots run synchronously when you click Create snapshot, and in the background via WP-Cron on the schedule you configure (default: weekly). Only one snapshot can run at a time — a concurrent trigger (manual click during a scheduled run, overlapping cron) is turned into a friendly "already running" notice instead of two builds trampling each other.

Chunk Store & Deduplication

All backup content lives in a content-addressed chunk store under wp-content/mhbackup-<random>/chunks/. Every chunk is named by its SHA-256 hash and stored in a fanout directory structure. The storage folder carries a random 128-bit suffix so its URL is unguessable on any web server, and deny rules for Apache and IIS (plus silent index.php files) are written and self-healed automatically; the plugin even probes its own folder over HTTP and warns you (with a ready-made nginx snippet) if it is still reachable.

This design has two important consequences:

  • Deduplication is automatic. If two snapshots reference the same file content, the chunk exists once on disk. Repeat snapshots of a mostly-unchanged site are nearly free.
  • Integrity is verifiable. A chunk's name is its hash — re-hashing a chunk immediately reveals any corruption.

When encryption is enabled, chunks are encrypted with authenticated encryption (AEAD) before being written.

Exclusions

Three exclusion layers are applied on top of each other:

  1. Hard excludes (always on) — the plugin's own storage root (wp-content/mhbackup-*), WP core update staging (wp-content/upgrade), and legacy backup folders
  2. Hard basename excludes (always on) — .git, .svn, .hg, node_modules
  3. User excludes — configured on the Data preferences tab

User exclusion entries are interpreted by a simple rule — whether the entry contains a slash:

EntryMeaningExample matches
wp-content/uploads/privatePath prefix from ABSPATHwp-content/uploads/private/foo.jpg
wp-content/plugins/*/cachePath glob (* and ?)wp-content/plugins/foo/cache/baz.txt
cacheBasename anywhere in the treewp-content/cache/, any folder named cache
error_*Basename glob anywhereerror_log, error_2026.log

Default excludes: the cache basename (cache folders anywhere) and the extensions log, tmp, bak, old, swp.

Database tables can be excluded from the continuous write capture on the same tab (snapshots still dump every table). Entries work with or without the WP prefix and match exactly — actionscheduler_logs and wp_actionscheduler_logs both skip that one table and nothing else; use an explicit wildcard for families (actionscheduler_*). Every skip is a hole in time-machine replay for that table, so add only truly uninteresting churn.

Incremental Backups

File Change Scanner

Between full snapshots, an incremental scanner keeps track of file changes. It walks the installation using the same exclusion rules as the snapshot scanner and diffs the result against the last known state — recording created, modified and deleted files, along with their content chunks.

Scans are triggered automatically after relevant WordPress events — plugin and theme installs/updates, theme switches, media uploads — and run periodically via cron. You can also click Scan for changes on the Overview tab to run one immediately.

Database Write Capture

The most distinctive feature of Must-Have Backup: a lightweight database dropin that captures every write operation — INSERT, UPDATE, DELETE, REPLACE, TRUNCATE, ALTER, DROP, CREATE, RENAME — as it happens.

How it works:

  • Continuous DB capture (time machine) on the Schedule tab — on by default — installs a db.php dropin into wp-content/
  • Captured operations are buffered in memory during the request and flushed once at shutdown as a single query — the overhead per page load is negligible
  • Timestamps are recorded with microsecond precision, and non-deterministic SQL functions (NOW(), RAND(), UUID()) are resolved before capture so replay is deterministic
  • Transaction-aware — writes inside a transaction are only logged on COMMIT; a ROLLBACK discards them, so a rolled-back WooCommerce checkout can never be replayed as a ghost order
  • The dropin recognizes foreign db.php files (HyperDB, LudicrousDB) and never overwrites them

The Schedule tab shows the capture status: whether the dropin is installed, whether it loaded in the current request, and the total number of operations captured.

Off-Site Delta Shipping

Incremental captures don't wait for the next full snapshot to leave the server. Every 30 minutes by default (Schedule tab → Push incremental to remote every N minutes; the background task checks every 15 minutes, so 15 is the shortest useful value, and 0 turns pushing off) the plugin ships the between-snapshot deltas to every configured destination:

  • New content chunks referenced by recent file changes — deduplicated against everything the destination already holds
  • The file and database change logs themselves, as compressed log archives

This means a time-machine restore point is off-site within minutes of the change happening — not days later at the next snapshot. On a wiped server, wp mh-backup delta import pulls the logs back so time-machine restore works again from the remote copy alone.

Restore

Restore Points & Activity Feed

The Restore tab leads with an activity feed, because you usually know what happened ("the update this morning broke checkout") — not the exact minute. The feed turns the captured change logs into human-readable restore points:

  • File and database events are merged into one time-ordered stream, curated (background noise like scheduler runs, session tokens and transients is hidden) and labeled by what they touched — orders, posts & pages, products, users, comments, media, plugin & theme files, settings
  • Events are clustered into short bursts; each burst is a card ("32 posts · 8 orders"), and full snapshots appear inline as green "Full backup" anchors
  • Picking a card sets the restore target and shows a plain-language "Undoes since then:" summary of everything the restore would roll back
  • Preview changes opens the full breakdown — per-category database and file change counts and sample file paths — before you commit to anything
  • Load older activity pages further back, and Jump to a date moves the feed straight to a chosen day — so a multi-month history stays navigable

If you already know the exact time, use the Pick a moment date-and-minute picker below the feed instead — it sets the same target the feed does.

Staged Restore

Restores never touch your live site directly. Clicking Restore into staging on the Timeline tab rebuilds the chosen snapshot in an isolated area:

  • Files are reassembled under wp-content/mhbackup/work/restore-*/ with their original permissions and timestamps
  • Database tables are recreated under a random staging prefix — completely separate from your live tables
  • If any chunks are missing locally, they are fetched automatically from your configured storage destinations — there is no "restore from destination X" picker to get wrong; restore is local-first and transparently sources any gaps from whichever destination has them

Your live site keeps running throughout. You can inspect the staged files and tables before deciding to go further — or discard the staging with one click.

Staging runs as a resumable, time-budgeted state machine: the admin UI polls it step by step with a progress bar, each request stays short so it never trips a host's request timeout, and a dropped request simply resumes from where it left off — multi-gigabyte sites stage reliably even on constrained shared hosting.

Promote, Confirm & Rollback

When you're satisfied with the staged restore, Promote (swap) makes it live:

  1. Pre-swap health check — before anything live is touched, the staged database must contain the WordPress core tables and a well-formed site URL; a truncated or half-imported staging set is refused here and can never be promoted
  2. Maintenance mode is enabled
  3. Files are renamed into place (renames are atomic and fast — the swap takes seconds, not minutes)
  4. A single RENAME TABLE statement swaps every table simultaneously
  5. Maintenance mode is lifted

If anything fails mid-swap — including the post-swap sanity check — the swap reverses itself automatically: the database rename is undone and the original files are put back, so a failed promotion always lands you on the original site, files and database together.

The previous live version isn't deleted — it's held in a temporary area. After verifying your site works:

  • Confirm — drops the temporary copies, the restore is final
  • Rollback — reverses the entire swap, putting the previous version back exactly as it was

The plugin's own backup data (mhbackup/ folder and tables) is preserved through the swap, so your backup history survives every restore.

Time-Machine Restore

With incremental capture enabled, you aren't limited to snapshot times — any moment in your captured history is a valid restore target. Whether the target comes from an activity-feed card, the Pick a moment picker, or a snapshot's Restore to time… action, the mechanics are the same:

  1. The plugin picks the newest snapshot at or before your target time as the baseline
  2. Every recorded file change up to the target moment is replayed on top
  3. Every captured database operation up to the target moment is replayed in order — with auto-increment IDs pinned to their original values, so replayed rows get the exact primary keys they had live

The result is staged exactly like a normal restore — with the same promote/confirm/rollback safety net.

Per-Table & Per-Path Exceptions

Sometimes you want different parts of your site restored to different times. The classic case: a content problem happened this morning, but you've received orders all day. Under the Advanced block on the Restore tab you can add exceptions on top of the base moment:

  • Table exception — pin one database table to its own moment (e.g. keep wc_orders at "now" while everything else goes back to 9:00 AM)
  • Path exception — pin one folder subtree to its own moment (e.g. keep wp-content/uploads current)

The whole combined state is staged in a single pass — one base moment plus any number of exceptions, each with its own target time. If the chosen moments could leave related data inconsistent, the UI warns you but leaves the decision to you.

Partial Restore

The inverse of the time machine: the live site stays exactly as it is, and only the items you pick rewind to an earlier moment — for undoing one bad table or one broken plugin folder. Use the Partial restore — put back just a few things card on the Restore tab:

  1. Pick a moment — the baseline snapshot at or before it is used, and captured changes are replayed forward for the picked items only
  2. Files & folders — the tree shows your files as they were at that moment; tick a whole folder or individual files
  3. Database tables (optional) — tick tables, or use the quick-pick buttons
  4. Click Build partial restore (staging), then Apply to live — selected items only

A folder you pick is replaced entirely with its state at that moment — anything added to it since is moved into the undo copy. Selected tables are swapped the same way. Everything you did not pick stays live and untouched, and the usual Confirm / Rollback step follows. Requires an active subscription.

Large Sites & Shared Hosting

Restore and migration are built to survive constrained shared hosting on multi-gigabyte sites:

  • Files stream at any size — restore reassembles content chunk by chunk straight to disk, so memory stays flat whether a file is 4 KB or 4 GB
  • Database imports stream statement by statement — a multi-GB table never has to fit in PHP's memory limit
  • Replay is batched — the change logs are walked in bounded batches with a persisted cursor
  • Staging is resumable — short polled requests with a progress bar; a dropped request resumes from its cursor

Practical prerequisites: a full restore needs roughly 2× the site size in free disk (staging holds a full copy, and the swap briefly parks the previous live copy alongside it). For the very largest sites, run the restore from WP-CLI — the command line has no web-request timeout at all.

Storage Destinations

Overview

Snapshots are produced locally, then shipped off-site by the storage subsystem. Add any number of destinations on the Storages tab — each snapshot is uploaded to every active destination via a background queue.

Supported destinations:

  • Local folder — another disk or mounted volume on the same server
  • SFTP / FTP / FTPS — classic server-to-server transfer (SFTP client bundled, no PHP extension needed)
  • S3-compatible — Amazon S3, Backblaze B2, Wasabi, DigitalOcean Spaces, MinIO
  • Dropbox — one-click connect
  • Google Drive — one-click connect
  • OneDrive — one-click connect
  • WebDAV — Nextcloud, ownCloud, Synology, Apache mod_dav

The manifest is always uploaded last, so an interrupted upload never leaves a remote copy that looks complete but isn't.

Several sites can share one destination. Every site writes under its own must-have-backup/<hostname>/ namespace inside the destination, so multiple WordPress installs can back up into the same bucket, folder or server without colliding — while each site's own snapshots still deduplicate against each other.

Local Storage & Remote-Only Mode

By default the chunk store on your server holds a full copy of every snapshot, so restores are fast and work even when no destination is reachable. If this server's disk is too small for your backups, switch to remote-only: Storages tab → Local storage on this server → Remote-only — keep only metadata on this server. It needs at least one enabled remote destination.

  • Chunks are deleted from the server as soon as a destination holds them; only metadata stays local
  • Snapshots stay listed and restorable — a restore first downloads what it needs, so it is slower and the destination must be reachable
  • Changes captured between snapshots still land on the server first and leave after the next upload
  • The card shows local chunk-store usage and a Free local space now button; the Manage tab's Local disk retention is ignored in this mode
  • Removing a destination that holds the only copy of a snapshot asks for an explicit second confirmation

From WP-CLI: wp mh-backup storage remote-only on|off and wp mh-backup storage local-gc — see Storage & Queue.

S3-Compatible

Native AWS Signature V4 implementation — no SDK bloat. Works with Amazon S3 and any S3-compatible service:

  • Amazon S3 — enter access key, secret, region and bucket
  • Backblaze B2 — use the S3-compatible endpoint from your B2 dashboard
  • Wasabi / DigitalOcean Spaces / MinIO — enter the custom endpoint URL (usually with path-style addressing enabled)

Create a dedicated, least-privilege IAM user scoped to the one bucket (list, put, get, delete) — never use your root keys. Add destination immediately round-trips a small canary object, so a wrong permission surfaces on the spot.

Dropbox

Two auth modes on the Add-destination form:

  • Connect with Dropbox (default) — one click, sign in, approve. Nothing to configure in the Dropbox developer console; backups land in the app's own scoped folder, and the plugin can't see anything else in your Dropbox.
  • Advanced: access token — create your own app in the Dropbox App Console and paste a generated access token, if you prefer not to use the one-click flow.

Google Drive

Two auth modes on the Add-destination form:

  • Connect with Google Drive (default) — one click, Google sign-in popup, approve. Uses the drive.file scope, so the plugin only ever sees files it created itself. Backups go to an auto-created "Must-Have Backup — <host>" folder. Nothing to configure in Google Cloud.
  • Advanced: service account — create a service account in Google Cloud Console, download its JSON key, share a Drive folder with the service account's email address, and paste the JSON + folder ID. No third-party relay involved.

OneDrive

Two auth modes on the Add-destination form:

  • Connect with OneDrive (default) — one click, sign in with your Microsoft account in the popup, approve. Nothing to configure in Azure; the app can only see files it creates. Optionally enter a Folder inside OneDrive.
  • Advanced — use your own Azure app instead — register an Azure AD app (Web), add the redirect URI shown on the form, grant Files.ReadWrite + offline_access, create a client secret, then use the form's built-in authorize → code-exchange helper to obtain the refresh token.

The one-click connectors (OneDrive, Google Drive, Dropbox) need a site connected to your musthaveplugins.com account with an active subscription — see the License card on the Overview tab.

WebDAV / Nextcloud

Connects to any WebDAV server with Basic authentication — Nextcloud, ownCloud, Synology NAS, or a plain Apache mod_dav share. Enter the base URL, username and password (for Nextcloud, use an app password).

SFTP / FTP / FTPS

A pure-PHP SFTP client is bundled, so SFTP works without any server-side PHP extensions. Plain FTP and explicit FTPS are also supported for legacy hosts.

SFTP is its own entry in the destination dropdown, with a Password / SSH key toggle — paste a private key (with an optional passphrase) instead of a password if your server is key-only. Note that the standalone Recovery Kit uses password auth for SFTP (it carries no SSH library); if your server is key-only, create a temporary password user for the recovery and remove it afterwards.

Upload Queue & Bundles

Uploads never block your site. Every snapshot enqueues one upload job per destination; a WP-Cron worker processes the queue every minute. The Storages tab shows live queue status with pending, running and failed counts.

For network destinations (SFTP, FTP, and cloud drivers), chunks are packed into ~64 MB bundle archives before transfer. Instead of uploading tens of thousands of small chunk files — slow on any protocol — a snapshot ships as a handful of large, fast transfers. Local destinations keep the chunk-per-file layout, where renames are cheap.

Bundles are built and shipped one part at a time — pack, upload, delete, repeat — so the temporary disk a snapshot upload needs stays bounded to one bundle (~64 MB), not the whole snapshot. This is what lets even a multi-gigabyte backup ship from a shared account with a tight disk quota.

Each destination keeps a small index of what it holds. If it ever drifts from reality (files deleted by hand on the remote, a restored server), the Sync button — or wp mh-backup storage reconcile — rebuilds the index from the remote itself. Sync also ships any pending between-snapshot delta right away.

Each destination shows a status badge: Uploading, Queued, Failed, Up to date or No backup yet. Sync overdue (with the number of waiting changes) means captured changes have been waiting longer than twice your push interval — usually WP-Cron is not firing or the destination is unreachable. It never appears when incremental pushing is turned off.

Removing a destination takes it off this site instantly; the backups already on its cloud storage are kept by default. Ticking the optional wipe checkbox also deletes the remote copy — and if that wipe can't complete, the destination is kept and the reason shown, so cloud data is never silently orphaned.

Encryption

Password Mode

The default encryption mode is designed to be a one-field setup:

  1. Go to the Encryption tab
  2. Choose a password — a live strength meter nudges you toward a strong one, and an eye-toggle lets you verify what you typed (no confirm-twice)
  3. Click Enable encryption

From that point on, every new snapshot is encrypted with XChaCha20-Poly1305 authenticated encryption. The password is verified with Argon2id — the same primitives used by modern password managers.

Backups run unattended (cron never prompts for a password); the password gates restore operations. Disabling encryption requires the current password. Existing encrypted snapshots stay encrypted.

Enabling encryption does not retroactively encrypt previously-taken snapshots — for a clean encrypted-only history, set up encryption before taking your first snapshot. Mixed histories still restore fine: every stored chunk carries a format marker, so plaintext and encrypted content can coexist and mode switches never strand old backups.

The threat model: encryption protects your backups from the storage provider — or a leaked storage API key. Chunks are encrypted before upload, so Amazon, Dropbox or your FTP host only ever see random bytes.

Envelope Mode

Envelope mode uses an RSA-4096 keypair: the public key stays on the server and wraps each chunk's own random encryption key — so deduplication keeps working across snapshots. The private key is shown once at setup for you to download and keep safe. Where it lives after that is your choice:

  • Stored on the server (default) — the private key is also kept in the WordPress database, so restores stay fully automatic. Same threat model as password mode: your backups are protected from the storage provider or a leaked storage API key.
  • Offline only (opt-in) — untick the store-key option at setup and the private key exists only in your offline copy. Then even a fully compromised server cannot decrypt previously created backups. During restore the UI asks for the key, uses it for that one request, and never persists it.

Key Handling

  • The master key is stored in the WordPress options table — not as a file under the web root, so a misconfigured server can never serve it over HTTP
  • An Argon2id verifier of your password gates the restore UI
  • Disabling encryption keeps the master key — it is random, not derived from your password, and deleting it would permanently destroy every already-taken encrypted backup. Re-enabling reuses it.
  • In password mode the Encryption tab shows your recovery key (Download recovery key / Copy to clipboard). The master key lives in this site's database; if the database is lost, only this recovery key can decrypt your backups during disaster recovery — the password cannot, because the key is random, not derived from it. Download it right after clicking Enable encryption and keep it off the server.
  • Losing the key means losing access to encrypted backups. There is no recovery path — this is by design. Store your Recovery Kit, your recovery key (or envelope private key) and your password in a safe place.

Retention & Management

The Manage Tab

The Manage tab shows the Snapshot × destination matrix: snapshots as rows, destinations as columns. Each cell shows whether that snapshot exists at that destination. Click a column header to sort, or filter the list to the snapshots present on one destination. Actions:

  • Pin / Unpin (click a destination cell) — protect a snapshot from retention sweeps at that destination
  • Delete from this destination (same cell menu) — remove the snapshot from that one destination
  • Delete local (per row, or tick several rows and click Delete selected (local)) — remove the local copy only; chunks still needed by other snapshots are kept and remote copies are not touched

To remove a snapshot completely, delete it locally and from each destination. In remote-only mode only metadata is local: delete via the destination cells, and the local entry disappears once the snapshot is gone from every destination.

Retention Rules

Retention is per-destination with a global fallback, plus a separate setting for this server's own disk. All of it lives in the Retention card on the Manage tab:

  • Global fallback (default 6) — how many snapshots each destination keeps when it has no override of its own. 0 = keep everything.
  • Per-destination override — one row per destination. Blank = follow the global fallback; 0 = keep everything at that destination.
  • Local disk (default 0 = keep every snapshot on this server) — with N > 0, older local snapshots are deleted from this server only once a remote destination already holds them; the last copy of a snapshot that has not been uploaded yet is never deleted. A snapshot pruned locally is no longer listed or restorable from wp-admin (the Recovery Kit can still reach the remote copy). Ignored while remote-only storage is on.

The full sweep runs automatically once a day. Run sweep now applies the per-destination keep-last-N only — the Local disk setting and the change-history cleanup run with the daily sweep (to trigger it immediately: wp cron event run mhbackup/retention_sweep). Deletes are refcounted: a chunk is only removed when no retained snapshot — or a shipped incremental delta — still references it, so retention can never corrupt a kept snapshot or hole a time-machine replay.

Change History Retention

The incremental change logs (the time-machine history, stored locally in the {prefix}mhbackup_dbops and {prefix}mhbackup_file_changes tables) have their own days-based retention — default 90 days — applied only by the daily sweep, not by Run sweep now. A row is pruned only when it is older than the window, older than your latest full snapshot and already received by every enabled destination — a lagging destination pauses the prune rather than losing its disaster-recovery replay. The same window also trims the change logs stored on your destinations; full snapshots are not affected. A time-machine restore to a moment whose history has been pruned is refused with a clear message.

The window has no field in the admin UI. It is the retention_incremental_days key inside the mhbackup_options option (0 = never prune), so set it with WP-CLI and then run the sweep:

wp option patch update mhbackup_options retention_incremental_days 30
wp cron event run mhbackup/retention_sweep

wp option update mhbackup_retention_incremental_days … creates an unrelated option and has no effect (remove it with wp option delete mhbackup_retention_incremental_days if you ran it).

  • Step down gradually on a busy site. The cleanup deletes all expired rows in one query, so lowering the window from 90 to 7 in one go can lock the table for a long time. Go in steps (e.g. 45 → 30 → 14), running the sweep after each, in a quiet window.
  • Mind your snapshot interval. Only history newer than your latest full snapshot is always kept; older history is restorable only while it reaches back to the snapshot before it. With weekly snapshots, a 7-day window leaves between 1 and 7 restorable days — don't go below 14 days.
  • Reclaiming disk space. The plugin doesn't run OPTIMIZE TABLE, and InnoDB doesn't shrink the table file on its own. After a large cleanup, run OPTIMIZE TABLE wp_mhbackup_dbops; (with your table prefix) once, in a quiet window; it needs free disk space roughly the size of the remaining table.

Routine background writes (for example WordPress rewriting its cron option) are captured on purpose, because a restore replays them too; they are hidden from the activity feed, and a shorter window keeps them from piling up.

Pinning Snapshots

Pinned snapshots are never touched by retention sweeps, regardless of the keep-last-N setting. Pin the snapshot you made right before a major upgrade, or the last known-good state of a client site — it stays until you unpin it.

Schedule & Data Preferences

Schedule Tab

  • Full snapshot every N days — default 7 = weekly; 1 = daily; 0 = manual-only, relying on incrementals
  • File change scan every N minutes — default 60 (minimum 5); uploads and plugin/theme changes also trigger a scan
  • Push incremental to remote every N minutes — default 30; 0 = disabled
  • Continuous DB capture (time machine) — on by default, with live dropin status; turning it off limits restores to snapshot points

A live "next auto-snapshot" line under the cadence input shows exactly when the next automatic snapshot will run.

Cron fallback: on hosts where DISABLE_WP_CRON is set but no system cron was configured to replace it, scheduled tasks would normally stall silently — exactly what a backup plugin must never do. The plugin detects this and kicks the due events itself on admin page loads (non-blocking, throttled to once a minute). With a working WP-Cron or system cron this is a no-op.

Data Preferences Tab

All the "what data, and what happens to it" settings in one place:

  • Files & folders — paths, folder names and glob patterns, plus Extensions to skip. See Exclusions for the pattern syntax.
  • Database tables — Tables to skip drops tables from the continuous write capture. Hide from the activity feed is different: those tables are still captured and restored, only hidden from the Restore tab's activity feed so they don't drown out real changes.
  • Non-prefixed tables — tables in the same database that don't start with this site's table prefix are backed up only if you tick them here. Very long table names may be rejected at restore time (MySQL's 64-character limit).
  • Uninstall behaviour — On uninstall, delete EVERYTHING (backups, chunks, tables, keys, options), off by default. The wipe is skipped while a restore is still waiting for Confirm or Rollback. See Deactivation & Uninstall.

Changes auto-save and are picked up immediately by the running scanners.

Disaster Recovery

The Recovery Kit

The Recovery Kit answers the worst-case question: what if WordPress won't load at all? A fatal error from a bad update, a broken database, a corrupted db.php — cases where no plugin-based restore can help, because the plugin itself can't run.

The kit is a single self-contained PHP file that never boots WordPress:

  • Generated in your account at musthaveplugins.com — you choose a password, which is baked into the file as a one-way Argon2id hash
  • Not stored on your site, no standing endpoint — it only exists while you're actively recovering
  • Reads database credentials directly from wp-config.php and connects with mysqli — a broken WordPress is irrelevant. Even a dropped database is fine: the backup's substance lives on disk, and the database is just the restore target
  • Restores from the local backup data on disk first, falling back to your remote destinations if the disk is gone — S3/S3-compatible, FTP/FTPS, SFTP (password auth), WebDAV, Dropbox, Google Drive and OneDrive are all supported directly in the tool
  • Handles every encryption mode: plaintext, password mode, and envelope mode — stored keys are read from the surviving database/disk automatically; an offline-only envelope key is pasted in
  • Never overwrites wp-config.php during the file restore — your live database credentials stay intact
  • Self-deletes after a successful recovery

Generate your Recovery Kit right after setting up backups, and store the file somewhere off the server — a password manager attachment, an encrypted USB stick, your laptop.

Generating Your Kit

The kit is generated from your account on musthaveplugins.com — it is available with every subscription that includes Must-Have Backup:

  1. Log in at musthaveplugins.com and go to My Account > Subscriptions
  2. On your Backup subscription, click the three-dot (⋯) menu in the top right corner
  3. Choose Site recovery kit from the menu
  4. Enter a recovery password — this is the password you will type into the tool when restoring, so pick one you can retrieve in a disaster (password manager, printed note in a safe)
  5. Click Generate & download recovery.php and store the file somewhere off the server

The password is embedded in the downloaded file as a one-way Argon2id hash — it cannot be recovered from the file, and we never see or store it. Forgot it? Just generate a new kit with a new password; the old file simply stops being useful.

Using recovery.php

  1. Upload recovery.php next to your site's wp-config.php (site root)
  2. Open it in a browser: https://yoursite.com/recovery.php
  3. Enter your Recovery Kit password
  4. Pick a snapshot from the list — or use Connect remote storage if the local disk is gone — and start the restore (offline-only envelope key: paste it in first)
  5. When finished, the tool deletes itself

Authentication is the password you chose when generating the kit, embedded as a one-way Argon2id hash, with per-IP rate limiting (6 attempts per hour) against brute force. If a plugin caused the breakage, deactivate it after the restore, before it runs again.

It never reports a restore it didn't achieve. If a file or table can't be reassembled because a chunk is missing from the source you're restoring from, the tool marks the restore incomplete, names exactly which file and which chunk is missing, and does not self-delete — so you can resolve the gap (try the other source: the snapshot list merges disk and remote) and re-run, resuming where it stopped. Because an incomplete restore keeps its staging for resume, the tool shows a Delete leftover data button to reclaim that space straight from recovery.php (WordPress may be down, so the plugin can't clean it for you).

Migration

Migration Bundle

The Migrate tab packages your site into exactly two files — a standalone installer.php and an archive.zip — so a move is two uploads, not thousands of loose files over FTP. Use it to move to a new host, a new domain, or both.

You can bundle the site as it was at any moment in time, exactly like a restore: pick a minute with the date picker, and the bundle is built from the baseline snapshot with all captured file and database changes replayed forward to that moment.

Keep the Migrate tab open while it builds. The bundle is built in short steps driven by the open tab (about 20 seconds of work per request, so it never trips a host's request timeout). If you close the tab, the build pauses; reopen the Migrate tab and the bundle resurfaces — the progress bar continues from where it stopped, or the download buttons appear if it had finished. A ready bundle stays available for 24 hours and then auto-deletes so it can't quietly eat disk; a discreet Delete bundle link removes it immediately.

Installer password (optional) — if set, installer.php asks for it before it connects to the database or writes any file on the target server.

Building from a moment in time needs an active subscription, just like restore. Building straight from an existing snapshot — the Build migration ZIP button next to each snapshot (Timeline tab, or the snapshot list on the Migrate tab) — works without one.

Running the Installer

  1. Upload both files — installer.php and archive.zip — into an empty directory on the target server (no need to extract anything)
  2. Open installer.php in a browser
  3. Enter the new database credentials and the new site URL, hit Install
  4. The installer extracts the files straight into place, imports the database streaming statement by statement (multi-GB tables are fine), and rewrites URLs across every text column

The search-replace is serialization-aware: PHP serialized values get correct length prefixes after replacement, and JSON-encoded and escaped URL variants are handled too — so theme options, widgets and page builder content survive the URL change intact. The installer self-deletes (and removes the archive) when done.

Plugin Rollback

A bad plugin update is the most common reason anyone needs a backup — so the plugin also attacks the problem directly. Every WordPress.org-hosted plugin on the Plugins screen gets a Rollback action link:

  1. Click Rollback in the plugin's row on the Plugins screen
  2. Pick the version you want from the list of previous releases
  3. The chosen version is installed over the current copy through the standard WordPress upgrader — if the plugin was active, it is reactivated

WordPress.org keeps every release's package available, so no backup restore is needed just to undo one plugin's update. Premium or custom plugins that aren't hosted on WordPress.org don't get the link — for those, use a regular restore or the time-machine. Rolling back doesn't undo database changes a plugin's update routine may have made; when in doubt, take a snapshot first.

Monitoring

Notifications

Configure alerts on the Notifications tab. Each channel is independent — leave a field blank to skip it:

  • Email addresses — add as many as you like; subject prefixed with [MH Backup / hostname] and a short human-readable summary
  • Slack incoming webhook URL
  • Discord webhook URL

Under When to notify:

  • On successful snapshot — off by default
  • On failure (snapshot, queue job, integrity) — on by default. Covers: snapshot failed, upload job failed, integrity check or restore drill failed, incremental pushes to a destination failing three times in a row, a gap in a destination's change history (a fresh full snapshot is requested automatically), and a restore that has been live for 72 hours without being confirmed or rolled back
  • When a destination is near capacity — on by default

Integrity Checks

A scheduled job samples stored chunks, re-hashes them (and decrypt-verifies when encryption is on) and compares against their content address. Any mismatch fires an integrity alert — you learn about silent corruption before you need the backup, not after.

Restore Drills

A restore drill stages your newest snapshot into a throwaway area, checks the result, and cleans up — regular proof that your backups actually restore. Run one any time with Run drill now in the Restore drill card on the Overview tab, which also shows the last result. Scheduled drills are opt-in and have no switch in the UI: enable them with wp option patch update mhbackup_options restore_drill_enabled 1; they then run every 30 days. A failed drill sends a failure notification.

Storage Quota Warnings

Every destination that reports its free space is checked during the daily sweep. At 80% full you get a quota warning notification (Notifications tab → When a destination is near capacity, on by default). At 95% full, snapshot uploads and incremental pushes to that destination are skipped until space is freed — delete old snapshots there or lower its retention. The thresholds are the quota_warning_percent and quota_stop_percent keys of the mhbackup_options option; there is no field for them in the UI.

WP-CLI

Overview

Everything the UI can do is also available under wp mh-backup. This makes system-cron scheduling, CI pipelines and server automation straightforward. The CLI is also the escape hatch for very large sites — it has no web-request timeout. As in the UI, restore commands require an active subscription; backup commands never do.

Snapshots & Restore

# Force a full snapshot right now
wp mh-backup snapshot --reason=deploy

# List snapshots (newest first)
wp mh-backup list --limit=20

# Stage a restore of a snapshot (does not swap)
wp mh-backup restore <snapshot_id>

# Time-machine stage: baseline + replay up to the given moment
wp mh-backup restore --time=<unix_timestamp>

# Verify integrity: sample chunks, re-hash, decrypt-verify
wp mh-backup verify

Storage & Queue

# Destinations
wp mh-backup storage list
wp mh-backup storage test <id>
wp mh-backup storage sync <id>     # enqueue all snapshots for upload
wp mh-backup storage reconcile <id> # rebuild the destination index from the remote

# Local storage
wp mh-backup storage remote-only          # show the mode and local chunk-store usage
wp mh-backup storage remote-only on|off   # switch remote-only storage (on frees local space right away)
wp mh-backup storage local-gc             # remote-only: drop local chunks a destination already holds

# Upload queue
wp mh-backup queue status
wp mh-backup queue run              # process up to 50 pending jobs
wp mh-backup queue reset            # reset stuck running jobs

# Incremental deltas
wp mh-backup delta ship             # push pending deltas to destinations
wp mh-backup delta import <id>   # disaster recovery: pull delta logs back

Automation Cookbook

Weekly full snapshot from system cron (bypassing WP-Cron):

0 3 * * 0 cd /var/www/site && wp mh-backup snapshot --reason=weekly-cron --allow-root

Nightly integrity verification:

0 4 * * * cd /var/www/site && wp mh-backup verify --allow-root

Snapshot before every deployment:

wp mh-backup snapshot --reason=pre-deploy && ./deploy.sh

Developer Reference

Everything the plugin exposes for programmatic integration lives under the mhbackup/ hook namespace. Options are read and written through global helpers, and every read/write passes through a filter/action pair, so any setting can be overridden or reacted to from code:

mhbackup_get_option($key, $default)    // read (filtered)
mhbackup_set_option($key, $value)      // in-memory only
mhbackup_update_option($key, $value)   // persists to DB (fires update actions)

Actions

Fired by the plugin — hook these to react to backup lifecycle events:

ActionArgumentsWhen
mhbackup/snapshot_created$snapshot_id, $manifestA full snapshot finished successfully
mhbackup/snapshot_failed$reason, $errorA snapshot attempt failed
mhbackup/queue_job_failed$job_id, $messageAn upload/queue job exhausted its retries
mhbackup/quota_warning$destination_id, $percentA destination crossed its quota warning threshold
mhbackup/integrity_failed$messageIntegrity check found corrupt chunks, or a restore drill failed
mhbackup/register_storage_drivers—Storage registry boot — register custom drivers here
mhbackup/option/{key}/updated$valueA specific option was persisted
mhbackup/option/updated$key, $valueAny option was persisted

Filters

FilterArgumentsPurpose
mhbackup/option/{key}$valueFilter every read of a specific option
mhbackup/option/not_set/{key}$defaultFilter the default returned when an option has never been set
mhbackup/update_option/{key}$valueFilter a value just before it is persisted
mhbackup/check_option$result, $key, $value, $compareOverride option comparison results
mhbackup/ajax_update/{key}_option$response, $valueFilter the JSON response of a settings-UI option save
mhbackup/has_active_subscription$active, $statusOverride the subscription check — add_filter('mhbackup/has_active_subscription', '__return_true') unlocks restore on dev/staging clones
mhbackup/api_url$urlOverride the licensing/relay API base URL at runtime
mhbackup/get_template$html, $templateFilter rendered template output
mhbackup/verbose_logfalseReturn true to get plugin log lines in debug.log even without WP_DEBUG
mhbackup/dev_skip_ssl_verifyfalseDev/test only: skip SSL verification on storage driver HTTP calls (self-signed local endpoints)

Cron Events

All recurring work runs on named WP-Cron events — you can trigger any of them manually (wp cron event run <hook>) or reschedule them:

HookDefault scheduleDoes
mhbackup/queue_workerevery minuteProcesses the upload/job queue
mhbackup/full_snapshotdaily (gated by the cadence setting)Automatic full snapshot
mhbackup/file_scanhourly (gated by the scan interval)Incremental file change scan
mhbackup/incremental_uploadevery 15 minutesShips file/DB deltas to destinations
mhbackup/retention_sweepdailyApplies retention policies + quota checks
mhbackup/integrity_checkdaily (gated)Samples and verifies stored chunks
mhbackup/restore_drilldaily (gated, opt-in)Test-restores the newest snapshot

Constants

Define these in wp-config.php before the plugin loads to override defaults:

ConstantDefaultPurpose
MH_BACKUP_API_URLhttps://api.musthaveplugins.com/backup/Licensing / OAuth-relay API base — point it at a staging API for development

Defined by the plugin (read-only, useful in integrations):

ConstantValue
MH_BACKUP_CAPmhbackup_admin — the capability gating the admin UI and AJAX; granted to administrators on activation. Grant it to other roles to give them access.
MH_BACKUP_STORAGE_DIRAbsolute path of the randomized storage root (wp-content/mhbackup-<hex>/)
MH_BACKUP_PLUGIN_DIR / MH_BACKUP_PLUGIN_URL / MH_BACKUP_PLUGIN_FILEStandard plugin path/URL helpers

Template Overrides

Every admin template is rendered from the plugin's templates/ directory and can be overridden by the active theme: copy the file to {your-theme}/must-have-backup/{template}.tpl.php and it takes precedence. The rendered output also passes through the mhbackup/get_template filter.

Custom Storage Drivers

Third-party destinations plug into the same registry as the built-in drivers. Extend the abstract driver, then register on the boot action:

class My_Backup_Driver extends MH_Backup_Storage_Driver {
    const SLUG  = 'mydriver';
    const LABEL = 'My Storage';

    // implement: describe(), put(), get(), delete(),
    //            ls(), head(), free_space(), test()
}

add_action('mhbackup/register_storage_drivers', function () {
    MH_Backup_Storage::register_driver(My_Backup_Driver::SLUG, 'My_Backup_Driver');
});

Driver contract invariants:

  • put() must be idempotent — if the remote already holds a byte-identical object at the path, return without re-uploading (chunks are content-addressed, so a size match is sufficient evidence)
  • delete() must tolerate missing objects — the retention sweep calls it speculatively
  • test() must round-trip a canary write, not just connect — credentials might lack write permission
  • free_space() may return null when the backend has no quota API; retention treats it as unlimited

Changelog

0.4.1 – 2026.10.06.
[FIX] Backups no longer corrupt values containing a percent sign. Every “%” in your content was being written into the backup as 66 characters of internal marker, which then came back as garbage on a restore — permalink settings, URLs with encoded characters and anything else holding a “%” were affected. Existing backups taken before this release are repaired automatically while they are restored or migrated.
[FIX] Migration bundles now write binary columns correctly. Binary data was quoted as text, which could mangle it or collapse distinct keys into one — the failure that stopped a migration build partway through the database with a “duplicate entry” error.
[FIX] A migration build that hits an error now says so and keeps the progress it made, instead of leaving a progress bar that quietly stops moving. The reason is shown when you come back to the tab, and the build resumes from where it stopped.
[FIX] Every moment inside the change-history window can now be restored, also in the days right after a scheduled snapshot.
[FIX] Old change history is deleted in small batches in the background, so a large cleanup no longer weighs on the database.
[FIX] “Run sweep now” applies every retention setting, including local disk and change history.
[NEW] “Leftovers” card under Restore: scan for and delete scratch tables, abandoned migration bundles, temp files and stale locks left behind by interrupted operations. The same sweep now also runs daily on its own.
[NEW] Anything still in use is never listed, and data preserved from before a restore is only ever reported — never deleted automatically.
[NEW] “Change history (days)” setting under Manage → Retention: choose how far back the time machine can restore to any moment.

0.4.0 – 2026.09.17.
[NEW] Remote-only local storage: keep only metadata on your server, every backed-up chunk lives on your destinations. For hosts whose disk is smaller than the backup.
[NEW] Snapshots stay listed and restorable in remote-only mode; a restore downloads what it needs first.
[NEW] “Free local space now” button and local chunk-store usage on the Storages tab; WP-CLI: wp mh-backup storage remote-only on|off.
[NEW] License card at the bottom of the Overview: connection status with Connect, Reconnect, Disconnect and Re-check.
[FIX] The “Sync overdue” badge follows the push interval you set, and stays off when pushing is disabled.
[FIX] Change history newer than your latest full backup is never pruned by retention — the time machine stays complete even with manual-only or rare snapshots.
[FIX] A restore to a moment whose history was pruned, or during which change capture was off, is refused with a clear message instead of silently restoring older data.
[FIX] The plugin now checks what is really on a destination before trusting its index — uploads, sync and cleanup no longer rely on stale records; Sync repairs the index.
[FIX] Per-destination retention counts only fully uploaded backups, so a failed upload can no longer push out your last restorable copy.
[FIX] Removing a destination that holds the only copy of a backup now requires an explicit second confirmation.
[FIX] Exclusion rules that would empty the backup (e.g. “*” or “wp-content”) are refused; an empty snapshot fails instead of reporting success.
[FIX] A Local destination inside the WordPress folder is refused — it would back itself up until the disk is full.
[FIX] Tables created after your last full backup (e.g. a newly installed plugin) now survive a time-machine restore; a multi-table DROP in the change log can no longer touch the live database while a restore is being prepared.
[FIX] Google Drive: same-named duplicate files (from concurrent syncs) no longer cause a stale index to be read; leftovers are cleaned up automatically.
[FIX] Importing a settings file no longer carries over destinations, encryption state or the site identity — those are set up on each site explicitly.
[FIX] Replacing an envelope public key with a different one now asks for confirmation (new backups would need the new key’s private half).
[FIX] Sync never wipes the destination index when the destination cannot be listed at all.
[FIX] Uploads and delta sync no longer re-download chunks a destination already holds.
[FIX] Migration bundles pull missing chunks back from a destination instead of failing mid-zip.
[FIX] A leftover maintenance flag can no longer strand the site — it is lifted automatically once nothing holds it.
[FIX] Confirm/Rollback survive slow servers: a timed-out request keeps running and the buttons follow up until it finishes.

0.3.2 – 2026.08.21.
[FIX] The staging prefix adapts to the longest table name, so MySQL’s 64-character table-name limit can never be exceeded — on any site.
[FIX] If a table name still cannot fit, the restore stops up front with a clear list instead of failing mid-import.
[UI] Error messages stay on screen until you dismiss them, and a failed staging shows its reason on the card.

0.3.1 – 2026.08.18
[FIX] Percent signs in data (permalink structure, SEO settings) no longer corrupt on restore — the capture log stored WordPress’s internal placeholder token instead of the % itself.
[FIX] Replay cleans the leaked tokens out of older logs too, so existing backups restore correctly; a daily task repairs the stored log.
[FIX] Before promoting a restore, the staged data is scanned for leftover tokens and the restore warns loudly if any are found.
[FIX] The db.php capture loader refreshes itself when a plugin update changes it.

0.3.0 – 2026.08.16.
[PERF] Deterministic database dumps — repeat snapshots of an unchanged site re-store and re-upload next to nothing.
[PERF] Backup chunks are compressed on disk and at every destination.
[PERF] Faster Sync: bundle indexes come from small sidecar files instead of re-downloading whole bundles.
[CHANGE] Existing backups stay restorable; the first snapshot after updating re-uploads the database once.
[CHANGE] Download a fresh Recovery Kit — older kits cannot read the new compressed backups.
[SECURITY] Recovery Kit validates chunk names inside downloaded bundles.
[UI] The extra-tables notice buttons now match in size on every admin page.
[FIX] Restore no longer fails on a duplicate-key row in the dump (busy non-InnoDB tables); the conflicting row is skipped and logged.
[FIX] Rollback no longer gets stuck on an unmovable wp-content folder the swap had already skipped (bind mounts, root-owned mu-plugins).
[FIX] Rollback moves each folder out and back in as a pair — a failure can affect at most one folder, never empty wp-content.
[FIX] Maintenance mode no longer expires after 10 minutes mid-operation; the admin who started it keeps access.
[FIX] Rollback restores the plugin activation list if WordPress emptied it while files were being moved.

0.2.4 – 2026.08.05.
[FIX] Restore failed with a “Duplicate foreign key constraint name” error on tables that use foreign keys.
[FIX] Restore could run out of memory on large sites.
[FIX] Picking a full backup as the restore point now restores that snapshot directly, without a slow replay.
[FIX] Restore promotion no longer fails on a wp-content folder it cannot move (bind mount, root-owned mu-plugins).
[UI] Clearer restore flow: the preview shows what will actually change, and a clear “ready to restore” step.

0.2.3 – 2026.07.23.
[BETA] First public beta release

This website uses cookies to enhance your browsing experience and ensure the site functions properly. By continuing to use this site, you acknowledge and accept our use of cookies.

Accept All Accept Required Only