# Symelo Backups and Migration user guide

For Symelo Backups and Migration version 1.0.0.

This guide explains how to back up your site, keep copies in the cloud, restore a backup (even when the site no longer loads), and move a site to a new domain or server. It covers what every option does and how to fix problems yourself. The last section, "For developers", covers wp-config.php constants, hooks, WP-CLI commands, the REST API and the backup file formats.

Need help? Write to office@symelo.com or visit [symelo.com](https://symelo.com/).

## Contents

1. [What it does](#what-it-does)
2. [Key terms](#key-terms)
3. [Requirements](#requirements)
4. [Quick start](#quick-start)
5. [Where to find everything](#where-to-find-everything)
6. [Backups tab](#backups-tab)
7. [Schedules tab](#schedules-tab)
8. [Destinations tab](#destinations-tab)
9. [Migration tab](#migration-tab)
10. [Recovery tab](#recovery-tab)
11. [Settings tab](#settings-tab)
12. [Support tab](#support-tab)
13. [Notices, emails and Site Health](#notices-emails-and-site-health)
14. [Setting up each destination](#setting-up-each-destination)
15. [Restoring](#restoring)
16. [Migrating and cloning](#migrating-and-cloning)
17. [Backups before updates](#backups-before-updates)
18. [Encryption](#encryption)
19. [Retention and disk space](#retention-and-disk-space)
20. [How-to recipes](#how-to-recipes)
21. [Troubleshooting](#troubleshooting)
22. [Privacy and security](#privacy-and-security)
23. [Deactivating and uninstalling](#deactivating-and-uninstalling)
24. [For developers](#for-developers)

---

## What it does

Symelo Backups and Migration keeps copies of your whole site somewhere safe and brings them back when you need them.

- **Backs up** your database, plugins, themes, media uploads, the rest of wp-content and, if you want, WordPress itself. You start backups yourself or let **schedules** make them, from every hour to once a month.
- Makes **incremental backups**: after a full backup, later backups only store the files that changed. A new full backup starts automatically every few days.
- **Copies backups to destinations**: Google Drive, Dropbox, OneDrive / Microsoft 365, Amazon S3 and S3-compatible storage (Wasabi, DigitalOcean Spaces, Scaleway, MinIO), Cloudflare R2, Backblaze B2, Microsoft Azure Blob Storage, SFTP, FTP/FTPS and email. One backup can go to several destinations.
- **Restores** a backup from the dashboard, the whole site or only some parts.
- **Restores even when the site is broken**, with a private **recovery link** that works without WordPress.
- **Backs up before updates**: the database and the plugin or theme being updated (or WordPress itself) are saved right before every update, automatic updates included.
- **Moves and clones sites**: a **migration package** can be installed on an empty server with the standalone **installer**, uploaded to another site, or pulled by another site with a **migration link**. Addresses are replaced automatically, also inside serialized data.
- **Encrypts** the database or the whole backup with your password.
- **Removes old backups** by rules you choose, without ever breaking an incremental chain.
- Emails you when a backup fails, adds **Site Health** checks, and has **WP-CLI** commands and a **REST API**.

All work runs in the background in short steps. You can close the page while a backup runs. The only exception is the restore itself: keep the restore page open until it says it's done.

Backups are ordinary `.tar.gz` archives and a gzipped SQL file. An unencrypted backup can be opened with standard tools, without the plugin.

## Key terms

| Term | Meaning |
|---|---|
| **Backup** | A copy of your site at one moment: a folder with a description (`manifest.json`), the database file, one or more file archives and a file list. |
| **Component** | A part of the site you can back up and restore on its own: **Database**, **Plugins**, **Themes**, **Media uploads**, **Other content** and **WordPress core**. |
| **Full backup** | Contains every selected file. It can be restored on its own. |
| **Incremental backup** | Contains only the files that are new or changed since the backup before it. The database is always saved in full. It needs the backups it builds on. |
| **Chain** | A full backup and the incremental backups built on it. Restoring an incremental backup uses the whole chain automatically. |
| **This server** | The private backup folder on your web server, `wp-content/uploads/symelo-backups-…` (the end of the name is random). Every backup is made here first. |
| **Destination** | Another place that keeps copies of your backups, for example Google Drive or an S3 bucket. If the server fails or is hacked, the copies there are still safe. |
| **Job** | A task that runs in the background: a backup, preparing a restore, downloading or deleting a backup. Jobs run one at a time; others wait in line. |
| **Migration package** | A full backup made for moving a site: every component plus wp-config.php. It comes with an installer and a migration link. |
| **Installer key** | A 24-character password shown once when you create a migration package. Nobody can run the package's installer without it. |
| **Installer** | `installer.php`, a single standalone file that sets up a migration package on a server, even one without WordPress. |
| **Migration link** | A long code starting with `symelo-migrate:` that lets another site download a migration package directly, server to server. It works for 48 hours. |
| **Recovery link** | A secret address that opens the restore tool without WordPress, so you can restore a backup stored on this server when the site is broken. |
| **Restorer** | The standalone restore tool. Restores always run in it, outside WordPress, because they replace WordPress's own database and files. |
| **Encryption** | Optional protection of backups with your password, applied before they leave the server. Without the password, encrypted backups can't be restored by anyone. |
| **Retention** | The rules for how many old backups are kept, on this server and at destinations. |
| **From another site** | A backup whose signature this site can't check: it was made by another site, or changed afterwards, or this site's security keys changed since. Restoring it needs your confirmation. |

## Requirements

- WordPress 6.4 or newer.
- PHP 7.4 or newer. On older PHP the plugin stays inactive and shows: "Symelo Backups and Migration needs PHP 7.4 or newer. This site runs PHP …, so the plugin is inactive."
- PHP extensions:
  - **zlib**: needed for every backup and restore (compression).
  - **mysqli**: needed by the restore tool to import the database. Almost every host has it.
  - **sodium**: strongly recommended. Encrypted backups use Argon2id to turn your password into a key, and only the sodium extension has it. Without it, the plugin falls back to WordPress's built-in sodium_compat library and PBKDF2. A backup encrypted on a server with sodium can only be decrypted on a server with sodium.
  - **SimpleXML**: needed for Amazon S3 and compatible storage, Backblaze B2 and Azure.
  - **ftp** (built with OpenSSL for FTPS): needed for the FTP / FTPS destination.
  - SFTP works without extensions (the plugin brings its own SFTP library). On PHP 7 it needs the setting `mbstring.func_overload` to be off.
- Write permission for the web server on `wp-content/uploads` (backups are stored there) and on `wp-content` (the restore tool and the recovery link are placed there).
- Disk space: the plugin won't start a backup with less than 50 MB free. Plan for roughly the compressed size of your site per full backup on this server, plus room to restore. See [Retention and disk space](#retention-and-disk-space).
- Background work: the site sends short requests to itself, with WP-Cron as a fallback. If your host blocks both, run a system cron job (see [Run backups from a system cron job](#run-backups-from-a-system-cron-job)).

Only administrators can see and use the plugin (the `manage_options` capability). Developers can change this; see [For developers](#for-developers).

**Multisite:** backups cover the whole network: the tables and files of every site. The plugin's screen is in the main site's dashboard and is only available to super admins. Activate the plugin network-wide or on the main site.

---

## Quick start

This gets you from installation to an automatic, offsite backup with a recovery link.

### 1. Install and activate

1. In wp-admin, go to **Plugins → Add New Plugin** and search for "Symelo Backups and Migration".
2. Click **Install Now**, then **Activate**.

To install from a .zip file instead, click **Upload Plugin** on the same screen, choose the file, click **Install Now**, then **Activate**.

A new **Backups** menu appears in the admin sidebar. The plugin creates its private backup folder and schedules its daily maintenance. Nothing is backed up yet.

### 2. Make your first backup

1. Go to **Backups**. The **Backups** tab opens and says "No backups yet. Make your first one below: it runs in the background and takes a few minutes."
2. In the **Back up now** card, leave the defaults: everything except **WordPress core**, type **Automatic (recommended)**.
3. Click **Back up now**.

You see "Backup started. It runs in the background, so you can close this page." and a progress bar. When it's done, the page says "The backup is finished." and the backup appears in the **Backups** list, stored at **This server**.

### 3. Add a destination

A backup that only lives on your server is lost if the server fails.

1. Click the **Destinations** tab. Because there's no destination yet, the **Add a destination** picker is open.
2. Click a service, for example **Amazon S3 and S3-compatible**, and fill in its settings. See [Setting up each destination](#setting-up-each-destination) for where to find each value.
3. Click **Save destination**. The plugin tests it right away: "Destination saved and tested: backups can be stored there."

For **Google Drive**, **Dropbox** and **OneDrive / Microsoft 365** you first create your own app with the provider. After saving, click **Connect …** and sign in.

### 4. Set up a schedule

1. Click the **Schedules** tab. The **New schedule** form is open.
2. The defaults make a good start: **Every day** at **03:00**, **Incremental (recommended)** with a **Full backup every** 7 days, your usual components, this server only (tick your destinations), and **Keep the newest** 7 backups.
3. Click **Add schedule**.

### 5. Create the recovery link

1. Click the **Recovery** tab and **Create recovery link**.
2. Copy the link into your password manager now. It is shown only once.

If a bad update ever leaves you with a white screen, open this link and restore the newest backup from before the problem.

### 6. Optional, but recommended

- **Settings → Encryption**: set a password and choose **Encrypt everything**, especially when backups go to a cloud provider. See [Encryption](#encryption).
- **Settings → Notifications**: check the **Send to** address. Failure emails are on by default.

---

## Where to find everything

**Backups** in the admin sidebar opens one screen with seven tabs. They switch instantly without reloading.

| Tab | What it's for |
|---|---|
| **Backups** | Status, **Back up now**, the list of backups (restore, download, delete), recent activity and logs. |
| **Schedules** | Automatic backups: how often, what, where to, how many to keep. |
| **Destinations** | Where copies of your backups go: add, test, turn off and remove destinations. |
| **Migration** | Create a migration package; import a site from a package, an FTP upload or a migration link. |
| **Recovery** | The recovery link for broken sites, and the installer for restoring without this site. |
| **Settings** | What to back up, encryption, retention, notifications, backups before updates, speed and uninstall behaviour. |
| **Guide** | This user guide, inside the plugin, with a contents list and a search box ("Find in this guide"). |
| **Support** | Help links, a system information report and contact details. |

The Plugins screen has **Backups** and **Settings** links under the plugin.

When a job is running, its live progress is shown below the tab bar, whichever tab is open.

---

## Backups tab

### Status summary

The top card shows how your backups are doing:

- "Your last backup was **3 hours ago** (date)." Backups before updates and backups from another site don't count here.
- "The last backup failed: …" with a **View log** link, when the newest backup job failed.
- "No backups yet. Make your first one below: it runs in the background and takes a few minutes."

Below it:

- "Next scheduled backup: … (in 5 hours)." or "No schedule yet, so backups only happen when you start them." with a **Set up a schedule** link.
- "Backups are only kept on this server. If the server fails, they are lost with it." with an **Add a destination** link, when you have no destination.

The **Back up now** button jumps to the form. Four figures follow: **Last backup**, **Next scheduled**, **Backups** (with their total size) and **Destinations**.

### Back up now

| Setting | What it does | Default |
|---|---|---|
| **What to back up** | The components to include (table below). | As set in **Settings → What to back up** |
| **Where to keep it** | **This server** is always included: "Always. Needed for quick restores and the recovery link; old copies are removed as set under Settings → Retention." Tick the destinations that should get a copy too. Destinations that are turned off or can't run on this server are greyed out. | Every working destination ticked |
| **Type** | **Automatic (recommended)** or **Full** (see below). | Automatic |
| **Encrypt this backup** | Only shown when an encryption password is set. "Uses the password from Settings → Encryption." If encryption is **Off** in the settings, ticking it encrypts everything. | Ticked when encryption is on in the settings |
| **Note** | Optional, for example "Before the redesign". Shown in the list. | Empty |

| Component | Description in the plugin | Default |
|---|---|---|
| **Database** | Posts, pages, comments, users, orders and settings. | On |
| **Plugins** | Everything in wp-content/plugins. | On |
| **Themes** | Everything in wp-content/themes. | On |
| **Media uploads** | Images and files in wp-content/uploads. | On |
| **Other content** | The rest of wp-content, like must-use plugins and translations. | On |
| **WordPress core** | wp-admin, wp-includes and the files in the main folder. Usually not needed: WordPress can be reinstalled any time. | Off |

Good to know:

- Must-use plugins (`wp-content/mu-plugins`) belong to **Other content**: they are saved and restored with it, not with **Plugins**.
- Folders in the WordPress folder other than wp-admin, wp-includes and wp-content are never backed up. Symbolic links aren't backed up either; the job log lists them.
- wp-config.php is saved separately when **Include wp-config.php** is on in the settings, and is never copied over your live file by a restore.

**Automatic or Full:**

- **Automatic (recommended)**: "Makes an incremental backup when a recent full one exists: only files that changed since the last backup are saved, which is much faster and smaller. Otherwise everything is saved. The database is always saved in full."
- **Full**: "Saves every file again. Takes longer, but doesn't depend on earlier backups."

With **Automatic**, the backup is incremental when all of this is true:

- an earlier backup you started yourself (not a scheduled one) has exactly the same file components;
- that backup's whole chain is complete at every destination you ticked (or on this server, when you ticked none);
- the chain's full backup is less than 7 days old, and the chain has fewer than 50 backups;
- if the earlier backup is encrypted with **Encrypt everything**, it was made with the current password.

Otherwise a full backup is made. A backup with only the database is always complete on its own; the list shows it as **Database**. A full backup without the database shows as **Files**.

Click **Back up now**. You see "Backup started. It runs in the background, so you can close this page." If another job is running: "Another job is running. This backup starts as soon as it is done."

A backup is refused right away when there's less than 50 MB of free disk space ("There is only … of free disk space on this server, which isn't enough to make a backup. Free up space or remove old backups first."), or when encryption is requested without a password ("Set a backup encryption password in Settings → Encryption first, or turn encryption off for this backup.").

### Progress

While a job runs, a card below the tab bar shows its title (for example **Backing up**), a status badge, a progress bar and what it's doing right now, for example "Backing up wp_posts (1,200 of 5,000 rows)" or "Uploading … to Google Drive". The note says "This runs in the background, so you can close this page."

- **Cancel** stops the job after asking "Cancel this job? Work done so far is discarded." An unfinished backup is removed; an upload that was in progress is stopped at the destination.
- **View log** opens the job's log.

While this screen is open, it also does a little of the work itself, so jobs keep moving even on hosts that block background requests.

A backup goes through these steps:

1. **Preparing the backup**: full or incremental is decided.
2. For incremental backups: **Getting the file list of the previous backup** (downloaded from a destination if needed).
3. **Backing up the database**, table by table, then **Checking the database backup**: the dump is read back and must end with its completion marker.
4. **Looking for files**, and for incremental backups **Comparing with the previous backup** (by size and modification time).
5. **Archiving files** into one or more `.tar.gz` parts.
6. **Writing the file index** and **Calculating checksums**. The signed `manifest.json` is written last: its presence means the backup is complete.
7. **Uploading** each file to each destination, the manifest last.
8. **Removing old backups** as the retention rules say.

When the job is done, the page reloads and shows the result:

- "The backup is finished." with **View log**.
- "The backup is finished. There were 2 warnings." (yellow). Warnings don't stop a backup: a skipped unreadable file, a symbolic link, tight disk space, or "Uploading to … failed: … The backup is still kept on this server."
- "The backup failed: …" with the reason.
- "The backup was cancelled."

### The list of backups

The **Backups** card lists every backup on this server and at your destinations, newest first (the newest 200).

| Column | Meaning |
|---|---|
| **Date** | When it was made, and how long ago. A lock icon means **Encrypted**. Backups made by another site also show "From https://other-site.com". |
| **Type** | **Full**, **Incremental**, **Database**, **Files**, **Pre-update** or **Migration**. **Incomplete** (red) when files are missing. "Scheduled" under it when a schedule made it. |
| **Size** | The size of this backup's own files (not the chain). |
| **Contents** | The components in it. |
| **Stored at** | **This server** and the names of the destinations that have a complete copy. |
| **Note** | Your note, or for example "Before updating the plugin Akismet". |

Actions:

| Action | When shown | What it does |
|---|---|---|
| **Restore** | The backup is complete on this server or at a destination that can send it back (every type except Email) | Opens the restore form. See [Restore from the dashboard](#restore-from-the-dashboard). |
| **Download** | The backup is on this server | Opens a menu with every file: **Manifest**, **Database**, **Files** (or **Files, part 1**, **part 2**…), **File index**, and for migration packages **Installer**. "To restore it elsewhere, download every file, including the manifest." |
| **Copy to this server** | The backup is only at destinations that can send it back | "Copies the backup from its destination to this server, so you can download it. Restoring doesn't need this." Its download links appear when the copy is done. |
| A note instead of **Restore** | The backup is only stored by email | "Stored by email only: download the attachments and upload them on the Migration tab to restore." See [Email](#email). |
| **Delete** | The backup is stored somewhere | Opens a menu: **Delete it from:** with a checkbox per place, and **Delete backup**. Asks "Delete this backup from the selected places? This can't be undone." When this server also holds an unfinished copy of a backup that is complete at a destination, the menu has the extra checkbox **This server (unfinished copy)**. |
| **Log** | The job that made it is still known (jobs are kept for 30 days) | Opens that job's log. |

Deleting:

- Runs in the background: "The backup is being deleted in the background."
- A backup that newer incremental backups build on can't be deleted first: "1 newer incremental backup builds on this one and can't be restored without it. Delete it first."
- A backup that a running job is using can't be deleted: "This backup is being used by a running job. Try again when it has finished."
- **Email** destinations can't delete emails. The backup disappears from the list for that destination, and the log says "Email can't delete files, so … stays there. Delete it there yourself if you need the space."

**Look for backups at destinations** (shown when you have destinations) opens a small panel. Click **Look for backups** in it to list this site's folder at every destination that is on and can list files (all except Email), and add the backups found there: "Looking for backups at your destinations…". Use it after restoring an older database or reinstalling the plugin.

By default it looks in the folder of this site's current address. To look in another folder, for example the one of this site's old address, fill in the optional **Site folder** ("After moving to a new address, enter the old one's folder, e.g. old-domain.com"). **Show the folders at my destinations** looks at your destinations and shows the other site folders with backups in them as buttons that fill in the field ("Folders of other sites: click one to use it.", or "No folders of other sites were found."). On big destinations this can take a while. See [After a domain change](#after-a-domain-change).

An incomplete backup on this server (one that stopped before its manifest was written) can't be restored. Click **Delete** next to it ("Delete this incomplete backup from this server? It can't be restored, and this can't be undone."), or leave it: unfinished backup folders are removed automatically after two days. A backup that a running job is still writing is never offered for deletion.

### Recent activity

The **Recent activity** card lists the last 12 jobs with **Started**, **Job** (**Backup**, **Restore**, **Import**, **Package download**, **Download**, **Delete**, **Cleanup**, **Destination check**, **Migration package**), **Status** (**Waiting to start**, **Running**, **Finished**, **Finished with warnings**, **Failed**, **Cancelled**), **Details** (the error, the progress, or "Took 3 mins" and the number of warnings) and a **View log** link.

While a prepared restore or import is waiting for you, its row says "Ready to restore. The link works until …" and has a **Continue to the restore** button. Only the administrator who started it sees the button, and only until the link expires (an hour after the backup was checked).

### Logs

**View log** opens the log in place, titled for example "Log: Backup, October 12, 2026 3:00 am". Each line has the time in UTC, a level and what happened. Passwords, keys and tokens are never written to logs. Logs and finished jobs are kept for 30 days; older ones say "This log isn't available any more. Logs are kept for 30 days."

---

## Schedules tab

"Schedules make backups for you. Times are in the site's timezone (…). WordPress starts them when the site gets visits, so on very quiet sites a backup can start a few minutes late."

If WP-Cron is turned off on your site, a warning says: "WP-Cron is turned off on this site (DISABLE_WP_CRON). Make sure your server calls wp-cron.php every few minutes, otherwise schedules don't run."

### Schedules list

| Column | Meaning |
|---|---|
| **Schedule** | The name (click to edit), with **Off** when it's turned off. |
| **When** | For example "Every day at 3:00 am", and "Incremental, full every 7 days" or "Full backups". |
| **Backs up** | The components, "To: …" (the destinations, or "This server") and how many are kept, for example "Keeps the newest 7". |
| **Next run** | Date and time, or — when off. |
| **Last run** | Date and time with a status: **Done**, **Done with warnings** (an upload failed), **Running**, **Skipped**, **Cancelled** or **Failed**. Below it the reason, if there is one (for example "Skipped: the previous backup of this schedule was still running." or why a backup failed), and a **View log** link to that run's log. "Not yet" before the first run. |

Actions: **Edit**, **Run now** (starts a backup with this schedule's settings now; the next regular run stays as it is; refused with "A backup for this schedule is already running." while one is queued or running), **Turn off** / **Turn on**, **Delete** ("Delete this schedule? Backups it made are kept.").

### Adding or editing a schedule

Click **Add schedule** (the form is already open when you have no schedule).

| Setting | What it does | Default |
|---|---|---|
| **Name** | Optional, for example "Nightly". Without one, the schedule is called "Daily backup", "Weekly backup" and so on. | Empty |
| **How often** | **Every hour**, **Every 6 hours**, **Every 12 hours**, **Every day**, **Every week** or **Every month**. | Every day |
| **Day of the week** | Only for **Every week**. | Monday |
| **Day of the month** | Only for **Every month**. "1 to 28, so the day exists in every month." | 1 |
| **Time** | "A quiet time of day is best. For hourly backups only the minutes count; every 6 or 12 hours counts from this time." | 03:00 |
| **Type** | **Incremental (recommended)**: "Saves only the files that changed since the last backup, which is fast and small, and makes a full backup now and then. The database is always saved in full." **Full every time**: "Every backup stands on its own. Uses more time and space." | Incremental |
| **Full backup every … days** | Only for incremental. A new full backup starts when the chain's full backup is this old. "Restoring an incremental backup needs the full backup it builds on, so those are kept too." 1 to 365. | 7 |
| **What to back up** | The components. Each schedule keeps its own choice. | As in **Settings → What to back up** |
| **Where to keep it** | **This server** (always) plus the destinations you tick. A turned-off destination shows "(turned off: skipped until you turn it on)". | This server only: tick your destinations |
| **How many to keep** | **Keep the newest … backups and delete them after … days**. "0 means no limit. Older backups from this schedule are removed after each new one." | 7 and 0 |
| **Status** | **On**. | On |

Click **Add schedule** or **Save schedule**.

How schedules work:

- Each schedule has its own chain of incremental backups. Manual backups and other schedules never mix into it.
- An incremental schedule also makes a full backup when the chain can't be continued: a destination was added, the components changed, a chain member is missing at a destination, or the chain reached 50 backups.
- If a schedule is due while its previous backup is still queued or running, that run is skipped (**Last run** shows **Skipped**).
- Different schedules that are due at the same time wait for each other: one job runs at a time.
- When a scheduled backup can't even start (for example no disk space, or encryption without a password), **Last run** shows **Failed**, and you get the email "[Site] Scheduled backup couldn't start" when failure emails are on.
- **How many to keep** applies to this schedule's backups at each of its destinations. When the schedule has no destination, it applies on this server too. The copies on this server always follow **Settings → Retention**. See [Retention and disk space](#retention-and-disk-space).

---

## Destinations tab

"Every backup is made on this server first. Destinations keep copies somewhere else, so you can still restore your site if the server fails or is hacked. Two places are safer than one."

### Destinations list

The first row is always **This server**: "Always on. Old copies are removed as set under Settings → Retention." It shows **Ready** and the free disk space, with a **Retention** link.

Each destination shows its name, type and folder, and a status:

| Status | Meaning |
|---|---|
| **Ready** | Settings complete. (Use **Test** to check the connection.) |
| **Connected** + account | Google Drive, Dropbox or OneDrive is signed in. |
| **Not connected** | Google Drive, Dropbox or OneDrive: open it and click **Connect …**. |
| **Reconnect needed** + reason | The sign-in was revoked or expired, or it can't be read any more: "The saved sign-in can't be read anymore (the site's security keys changed). Connect again." Open the destination and click **Reconnect**. |
| **Off** | "Schedules skip it." Manual backups can't choose it either. |
| **Needs attention** + problems | A required value is missing, or "A saved password or key can't be decrypted any more (the site's security keys changed). Enter it again." |
| **Not available** + reason | This server can't use this type, for example "This server's PHP has no FTP support (the ftp extension). Ask your host to enable it, or use SFTP." |
| **Unknown type** | "The add-on that provided it may be inactive." |

Actions: **Edit**, **Test**, **Turn off** / **Turn on**, **Remove** ("Remove this destination? Backups already stored there stay there, but this site stops using it.").

**Test** uploads a small file, finds it in the listing, reads it back and deletes it: "It works: a test file was uploaded and deleted again." or "The test failed: …".

### Adding a destination

1. Click **Add destination** (the picker is already open when you have none).
2. Under **Add a destination**, click a service tile: **Google Drive**, **Dropbox**, **OneDrive / Microsoft 365**, **Amazon S3 and S3-compatible**, **Cloudflare R2**, **Backblaze B2**, **Microsoft Azure Blob Storage**, **SFTP**, **FTP / FTPS** or **Email**. Tiles the server can't use are greyed out with the reason. Tiles also show what the provider gives away, for example **10 GB free** (green: free every month) or **Free trials** (blue: for a limited time), and the settings card explains the offer with a link to the provider's current prices. See [Free storage at a glance](#free-storage-at-a-glance).
3. Fill in the service's fields. Required fields have a red asterisk (*). Each field's help text says where to find the value; see [Setting up each destination](#setting-up-each-destination).
4. Optionally enter a **Name** ("Only shown in this plugin. Defaults to the service's name."), for example "Google Drive (office account)".
5. Click **Save destination**.

After saving:

- Most destinations are tested right away: "Destination saved and tested: backups can be stored there." or "Saved, but the connection test failed: …" (the settings are kept, so you can fix them).
- Google Drive, Dropbox, OneDrive: "Saved. Now click Connect to sign in to your account." You stay on the destination's page.
- SFTP without a host key fingerprint: "The server sent its host key. Check it below and click "Trust this key" if it is the right one." See [SFTP](#sftp).

Every destination except Email has a **Folder** (default `SymeloBackups`): "Backups are stored in this folder, in a subfolder named after this site (for example SymeloBackups/example.com/), so several sites can share one destination. Use slashes for subfolders." The subfolder is named after the site's address (host, port and path).

### Editing a destination

Click its name or **Edit**.

- Passwords, keys and secrets are never shown again. The field says "Saved. Leave blank to keep it." Type a new value to replace it. Optional secrets (for example a Dropbox app secret or an SSH key passphrase) have a **Remove the saved value** checkbox.
- The type of a destination can't be changed: add a new one instead.
- Google Drive, Dropbox and OneDrive show an **Account** card: **Connected** with "Backups are stored in you@example.com." and **Disconnect**; or **Reconnect needed** with the reason, **Reconnect** and **Disconnect**; or "Not connected yet. Save the client ID and secret below, then sign in to allow this site to store backups in your account." with **Connect …**. While a required setting is still empty (for example the OneDrive **Tenant**), it says "Fill in the settings marked * below and save them. Then you can sign in to allow this site to store backups in your account." and the destination shows **Needs attention** with, for example, "Tenant is required."
- The **Test, turn off or remove** card: "The test uploads a tiny file, reads the folder and deletes the file again. A destination that is turned off is skipped by schedules but keeps its settings." Buttons: **Test connection**, **Turn off** / **Turn on**, **Remove destination**.

Turning a destination off is useful during maintenance of the storage: schedules skip it and say so in their log ("The destination "…" is turned off and was skipped."), and nothing else changes.

You can have up to 50 destinations.

---

## Migration tab

"A migration package is a complete copy of this site (files, database and settings) that another site can import. Use it to move to a new domain or host, or to copy a site into this one."

See [Migrating and cloning](#migrating-and-cloning) for complete walkthroughs.

### Export this site

"Creates a package with everything, including wp-config.php. When it is ready you get three ways to move it: download the files, use the installer on a new server, or send the migration link to another site."

When encryption is on: "Encryption is on, so the package is encrypted. You'll need your encryption password on the other site."

Click **Create migration package**. It runs in the background like a backup: "Creating the migration package. It runs in the background; the download links appear here when it is ready."

When it's done, the **Your migration package is ready** card shows:

- **Save the installer key now.** "It is shown only this once. You need it to run installer.php on a new server; nobody can use the installer without it." The **Installer key** field has a **Copy** button. After a reload: "The installer key was shown when the package was created and isn't shown again. If you didn't save it, create a new package."
- **Download the package**: "Download every file. For a new server, also download the installer." The files, plus **Installer** (`installer.php`).
- **Or send it straight to another site**: the **Migration link** with a **Copy** button. "On the other site, install this plugin and paste the link under Migration → Pull from another site. The link works until …. Anyone who has it can download the package, so keep it to yourself."

**Packages on this server** lists up to five packages with their download links. "Packages are listed on the Backups tab too, where you can delete them."

Each package there also has a **New migration link** button: "For pulling this package to another site, server to server. Links you made before keep working until they expire." Use it when the first link expired or got lost. The page then shows "New migration link for the package from …": "Copy the link now. It is shown only this once. If you lose it, create another one." Like the first one, it works for 48 hours.

Migration packages are only kept on this server; they aren't uploaded to destinations. They count towards the local retention rule (see [Retention and disk space](#retention-and-disk-space)).

### Moving to a new server

The card repeats the steps for an empty server:

1. "Create a migration package and save the installer key."
2. "Download all package files and installer.php."
3. "On the new server, create an empty database, then upload installer.php and the package files to the folder where the site should live."
4. "Open installer.php in your browser (for example https://example.com/installer.php), enter the installer key and the new database details."
5. "When it is done, log in as usual. Check that installer.php and the package files are gone from the server."

"If WordPress is already installed there, install this plugin and import the package (below)."

### Import a site into this one

"Importing replaces everything on this site (content, users, plugins, themes and settings) with the other site. Afterwards you log in with the other site's usernames and passwords."

There are three ways to get a package here. They all end up under **Packages ready to import**.

**Upload from your computer**

"Choose all files of the package: manifest.json and the .gz files (.enc when the package is encrypted). Big files are sent in small pieces, so your host's upload limit doesn't matter, and an interrupted upload continues where it stopped."

1. Choose the files (several at once is fine).
2. Click **Upload**. Each file shows its progress ("12 MB of 480 MB"). **Pause** and **Resume** are available.
3. When a package is complete: "Package … is complete." with an **Import this package** button. Otherwise "Still missing: …".

If the upload stops, choose the same files again: the upload continues where it stopped. Files that aren't package files are refused ("Not a backup package file. Choose manifest.json and the .gz or .enc files of a backup."). The upload needs this much free disk space: the file's size plus 50 MB.

**Files uploaded by FTP**

"With FTP or your host's file manager, create a folder named after the backup ID inside the folder below, and put all package files in it. The backup ID is the start of the file names, like 20261012-191500-migration-a1b2c3." The **Import folder** field shows the folder, for example `wp-content/uploads/symelo-backups-…/import/`. "Then click "Look for packages" below."

Upload in binary mode, so the files don't get changed on the way.

**Pull from another site**

"On the other site, create a migration package and copy its migration link. This site then downloads the package directly, server to server, which is the fastest way."

Paste the link and click **Download package**: "Downloading the package from the other site. It appears under "Packages ready to import" when it is done." Every file's checksum is checked. A migration link can't be used on the site that made it: "This migration link is from this site. Paste it into the site you are moving to."

**Packages ready to import**

**Look for packages** refreshes the list. Each package shows **Package** (date and ID), **From** (the old address), **Size** and **Status**: **Complete**, or **Files missing** with the missing file names. Click **Import** next to a complete package. **Delete** removes a package's files from the import folder: "Delete this package? Its files are removed from the import folder on this server. This can't be undone."

The **Import this package** form shows **Made on**, **Size** and **Links** (old address → this site's address), and two warnings:

- "Everything on this site is replaced. Posts, pages, users, plugins, themes, media and settings all come from the package. Afterwards you log in with a username and password from the other site. Make a backup of this site first if you might need it."
- "Your site will be in maintenance mode during the import. Keep this browser tab open until it says it's done."

"Packages from another site can't be verified here, so only import packages you made yourself or trust."

Enter the **Encryption password** ("Only needed when the package is encrypted: enter the encryption password of the site that made it."), tick "I understand that this site's content and users will be replaced." and click **Import and replace this site**. It asks once more: "Import this package and replace this site? This can't be undone without a backup."

The package is checked ("Getting the package ready. The import starts as soon as it has been checked."), then your browser continues to the restore page, as for a restore. See [What happens during a restore](#what-happens-during-a-restore).

An import is refused before anything changes when the package doesn't fit this site: a multisite package on a single site, a single-site package on a multisite network, or a network package from another domain or path. See [Multisite](#multisite).

Packages stay in the import folder after an import. Delete them under **Packages ready to import** when you no longer need them.

---

## Recovery tab

"A secret address that restores this site from a backup on this server, even when WordPress itself is broken: a white screen after an update, a crashed plugin, or a hacked site. It works without logging in, so treat it like a password."

### Creating the link

Click **Create recovery link**. The page shows: "Save this link in your password manager now. It is shown only this once. If you lose it, create a new one." with the **Recovery link** and a **Copy** button.

The link looks like `https://example.com/wp-content/symelo-recovery/index.php?key=…`.

The status card then says "The recovery link is on. It was created on …" and how many backups it can restore: "3 backups on this server can be restored with it." Without local backups: "There are no backups on this server yet, so the link has nothing to restore. Keep at least one copy on this server."

| Button | What it does |
|---|---|
| **Create a new link** | Replaces the link; the old one stops working. Asks "Create a new recovery link? The current link stops working." |
| **Turn off** | Removes the recovery tool; the link stops working right away. |

The recovery tool lives in `wp-content/symelo-recovery/`. If a redeploy removes it, or the database details, paths, plugin version or encryption password change, the plugin sets it up again with the same link the next time a wp-admin page is opened. If that isn't possible, the card says "The recovery files are missing or can't be written (a deploy may have removed them). Create a new link to set them up again."

### When your site is broken

1. "Open the recovery link from your password manager."
2. "Choose the newest backup from before the problem started."
3. "Follow the steps. The files and database are replaced just like a normal restore, then you can log in again."

"The link only uses backups stored on this server. If the server itself is gone, install WordPress and this plugin on a new server, add the same destination, click "Look for backups at destinations" on the Backups tab (enter the old site folder if the address changed) and restore from there. Or move a migration package with its installer." See [When the site is broken: the recovery link](#when-the-site-is-broken-the-recovery-link) and [On a new server](#on-a-new-server-installerphp) for the details.

### Restore without this site

"A migration package comes with installer.php, a standalone restorer that works on any server, even an empty one. Keep a recent package somewhere safe to be ready for the worst case."

When you have a package on this server, the card shows "Newest package on this server: …" with its download links. "The installer asks for the installer key shown when the package was created." The button **Create a migration package** (or **Create a newer package**) opens the Migration tab.

The installer only works with migration packages, and packages stay on this server. To be ready for a total server loss, download a recent package (all files plus `installer.php`) and keep it somewhere else, together with its installer key.

---

## Settings tab

The settings are grouped in cards: **What to back up**, **Encryption**, **Retention**, **Notifications**, **Before updates** and **Advanced**. The links at the top jump to each card. Each card has its own **Save changes** button: "Settings saved."

### What to back up

"Used by "Back up now" and new schedules. Each schedule keeps its own choice."

| Setting | What it does | Default |
|---|---|---|
| Components | **Database**, **Plugins**, **Themes**, **Media uploads**, **Other content**, **WordPress core**. See [Back up now](#back-up-now). At least one is needed. | All except WordPress core |
| **Include wp-config.php** | "It holds your database login and security keys. It is stored in the backup but never copied over your live file automatically, so a restore can't lock you out." The installer uses it to set up the new site. Only saved when the backup includes files. | On |

**Leave out**

| Setting | What it does | Default |
|---|---|---|
| **Folders and files** | "One per line, relative to the WordPress folder. * matches anything, so */node_modules leaves out every node_modules folder. The defaults skip caches and other backup plugins' archives." Leaving out a folder leaves out everything in it. Up to 200 lines. **Use the defaults** fills in the default list again. | See below |
| **File types** | Extensions without the dot, separated by commas. | `log, tmp` |
| **Files larger than … MB** | "A size of 0 means no limit." | 0 |

The default exclusions:

```
wp-content/cache
wp-content/upgrade
wp-content/upgrade-temp-backup
wp-content/updraft
wp-content/ai1wm-backups
wp-content/backups-dup-lite
wp-content/backups-dup-pro
wp-content/wpvividbackups
wp-content/uploads/backwpup-*
*/node_modules
*/.git
```

Patterns match the whole path. For example `wp-content/uploads/2019` leaves out that year's uploads, and `*.zip` leaves out every .zip file. Patterns may contain letters, digits, spaces and `. _ * / @ + -`, but not `..`; others are refused: '"…" isn't a valid path to exclude and was left out.'

Always left out, whatever you set: the plugin's own backup folder, the restore and recovery tools, `.maintenance`, and wp-config.php (saved separately, see above).

**Database tables**

| Option | Description in the plugin |
|---|---|
| **This site's tables (starting with wp_)** (default) | "Recommended. Tables from other software in the same database are left out." On multisite this includes the tables of every site. |
| **All tables in the database** | "Includes tables that don't belong to WordPress." |
| **Only the tables I choose** | "For special setups. Leaving out WordPress tables can make a restore incomplete." A list of every table with its size appears; tick the ones to back up. |

### Encryption settings

"Encrypted backups can only be opened with your password, so a leaked or stolen backup reveals nothing. They are encrypted before they leave this server."

| Option | Description in the plugin |
|---|---|
| **Off** (default) | "Backups can be opened with ordinary tools." |
| **Encrypt the database** | "Protects user accounts, orders and settings. Files stay readable." |
| **Encrypt everything** | "The database and all files. The safest choice when backups are stored with a cloud provider." |

Under **Password**, enter a **New password** (at least 8 characters; "a long passphrase is best") and **Repeat it**. A red callout reminds you: "If you lose this password, your encrypted backups can't be restored by anyone, including us. Save it in your password manager. You also need it to restore a backup on another site or server."

Choosing a mode without a password doesn't turn encryption on: "Set a password to turn on encryption." Once a password is set, the card shows **A password is set** ("Leave the fields blank to keep it.") and a **Remove the password** card. See [Encryption](#encryption).

### Retention

"How many backups stay on this server. Copies at destinations follow each schedule's own setting."

| Setting | Default |
|---|---|
| **Keep the newest … backups** (0 to 1000) | 5 |
| **and delete them after … days** (0 to 3650) | 0 |

"0 means no limit. Backups take disk space, so keep only what you need; the full backups that newer incremental ones build on are kept automatically." The card also shows the free disk space. See [Retention and disk space](#retention-and-disk-space).

### Notifications

| Setting | What it does | Default |
|---|---|---|
| **Email me when a backup fails** | Also when a backup finished but couldn't be copied to a destination, when a scheduled backup couldn't start, and when downloading a migration package fails. | On |
| **Also email me when a backup succeeds** | Not for backups before updates. | Off |
| **Send to** | "Separate several addresses with commas. Emails are sent with wp_mail(), so an SMTP plugin like Symelo SMTP improves delivery." Up to 10 addresses. | The site's admin email |

See [Notices, emails and Site Health](#notices-emails-and-site-health) for what the emails contain.

### Before updates

| Setting | What it does | Default |
|---|---|---|
| **Back up before updates** | "Before WordPress updates a plugin, a theme or itself (also automatic updates), a quick backup of the database and of what is being updated is made, so a bad update can be undone. It adds a few seconds to each update." | On |
| **Keep the newest … of these backups** (1 to 50) | Older backups before updates are removed. | 3 |

Below the settings, the card shows how the last backup before an update went: "Last backup before an update: … (… ago)" with **Done**, **Skipped** or **Failed**, and what happened, for example "Backed up before updating the plugin Akismet." or "No backup was made before updating the plugin Akismet because another backup was running. The update went ahead." Skipped and failed results are highlighted. Before the first update: "No backup has been made before an update yet."

See [Backups before updates](#backups-before-updates).

### Advanced

**Speed**: "Backups run in short steps in the background. This sets how hard they may use the server."

| Speed | Description in the plugin | Length of a step | Pause between steps | Waits when the load per CPU core is at least |
|---|---|---|---|---|
| **Fast** | "Long steps, one after another. For dedicated servers, or quiet times." | 25 s | None | Never |
| **Normal (recommended)** (default) | "Steady work that pauses when the server is overloaded." | 15 s | None | 2.0 |
| **Gentle, for small servers** | "Short steps with pauses in between, and waits whenever the server is busy. Backups take longer, but visitors won't notice." | 6 s | 20 s | 0.7 |

- A step never runs longer than PHP's time limit allows (the time limit minus 10 seconds, at least 5 seconds).
- The load check needs a server that reports its load. Where it can't (for example on Windows), work never waits.
- While the Backups screen is open, it also runs short steps of 3 to 8 seconds.
- WP-CLI, the REST API's `run` route and backups before updates don't pause and don't wait for a busy server.
- If the speed is set in wp-config.php: "Set by SYMELO_BACKUPS_SPEED in wp-config.php."

**Archives**

| Setting | What it does | Default |
|---|---|---|
| **Split archives into parts of … MB** (10 to 4096) | "Smaller parts upload more reliably to some destinations and fit email size limits. A single large file can still make a part bigger." | 500 |
| **Compression** (1 to 9) | "1 is fastest, 9 makes the smallest files. 6 is a good balance. Images and videos hardly shrink at any level." | 6 |

**When the plugin is deleted**

| Setting | What it does | Default |
|---|---|---|
| **Also delete its data** | "Removes the settings, schedules, destinations and sign-ins when you delete the plugin. Backups are never deleted: the ones on this server and at your destinations stay where they are, so you can still restore them." | Off |

Whatever you choose, deleting the plugin always removes the recovery link and leftover restore tools. See [Deactivating and uninstalling](#deactivating-and-uninstalling).

---

## Support tab

- **Get help**: "Most problems come down to disk space, a destination's login details, or background requests being blocked by the host. The log of a failed job usually says exactly what went wrong." Links to **Recent activity and logs**, **Test your destinations** and **Check Site Health**, and an **Email support** button (office@symelo.com).
- **System information**: a plain-text report with the plugin version, the storage folder (its random part masked) and whether it's writable, free disk space, backups, schedules, the current and recent jobs, your settings, destinations (names and types only), WordPress and server details, PHP extensions (including whether Argon2id is available) and disabled functions. "It never includes passwords, keys, tokens or links." Click **Copy system information** and paste it into your support email.
- **About Symelo LTD**: who makes the plugin, with a link to symelo.com.

---

## Notices, emails and Site Health

### Admin notices

On the Dashboard and the Plugins screen, when the newest backup job failed:

"Symelo Backups: the last backup (date) failed: …" with a **See what happened** link to its log. The notice disappears after the next successful backup.

On the plugin's own screen, when the key that signs your backups had to be replaced:

"The key that signs your backups had to be replaced (the site's security keys changed). Backups made before … will show as "from another site", and restoring them needs one extra confirmation." Click **Dismiss** once you've read it. See [Backups marked as from another site](#backups-marked-as-from-another-site).

### Emails

Emails go to the **Send to** addresses in **Settings → Notifications**, as plain text through `wp_mail()`.

| Subject | When |
|---|---|
| "[Site] Backup failed" | A backup failed. "No new backup was saved; your earlier backups are not affected." |
| "[Site] Backup finished, but an upload failed" | The backup is complete on this server, but couldn't be copied to every destination. |
| "[Site] Backup complete" | Only with **Also email me when a backup succeeds**. |
| "[Site] Scheduled backup couldn't start" | A schedule was due but its backup couldn't start, with the reason. |
| "[Site] Migration package download failed" / "[Site] Migration package downloaded" | A pull with a migration link failed or finished. |

Backup emails list the backup ID, who started it (an administrator, a schedule, an update or a migration export), the components, the start time, duration and size, each destination ("uploaded" or "failed (…)"), up to 10 warnings, and a link to the Backups screen.

### Site Health

**Tools → Site Health** includes six checks:

| Check | Good result | Problem and fix |
|---|---|---|
| Your site has a recent backup | "The last successful backup was … ago." | "Your site has no backup yet" (make one and set up a schedule). "Scheduled backups aren't running": the last backup is more than twice your most frequent schedule's interval old. Check the Backups screen for failed jobs and make sure WP-Cron runs. |
| Backups can't be downloaded from the web | "The web server refuses requests for files in the backups folder." | "The backups folder can be opened from the web": the server ignores the protection files (Nginx, for example). See [Privacy and security](#privacy-and-security). "The protection of the backups folder couldn't be checked": the site couldn't request a test file from itself. |
| Backup destinations work | "No destination reported an error recently." | "A backup destination has problems", with each destination's error from the last backup or listing. |
| There is enough disk space for backups | "… of disk space is free; the last backup needed …" | "Disk space for backups is running low" (less than twice the last backup) or "There isn't enough disk space for the next backup" (less than the last backup). |
| Backups run in the background | "No backup job is stuck." | "A backup job is stuck": a job hasn't made progress for 30 minutes. See [Jobs don't start or get stuck](#jobs-dont-start-or-get-stuck). |
| This site can recognize its own backups | "The key that marks backups as made by this site is intact, so restores can check that a backup wasn't changed." | "Older backups show as made by another site": the signing key couldn't be read any more (the security keys in wp-config.php or `SYMELO_BACKUPS_KEY` changed, or another site's database was imported), so a new one was made on the date shown. Older backups still restore, after a confirmation. Dismissing the notice on the plugin's screen also clears this result. See [Backups marked as from another site](#backups-marked-as-from-another-site). |

The protection check is remembered for 12 hours.

**Site Health → Info → Symelo Backups and Migration** lists the version, the backups folder, free disk space, the number of backups (all, and how many of them are on this server), the last successful backup, schedules, destinations, encryption, speed, whether backups before updates and the recovery link are on, when the background runner last ran (and any error sending its request), and whether the scheduler event exists.

---

## Setting up each destination

General tips:

- Create a separate account, key or app just for backups, with access to the backup location only.
- Keep buckets, containers and folders private.
- After saving, check that the test passes. A destination's problems also show in the Backups list (missing copies) and in Site Health.
- Provider dashboards change often. If a menu name below doesn't match exactly, look for the closest equivalent.

### Free storage at a glance

Several providers store backups for free, which is enough for many small and medium sites, especially with incremental backups and retention rules. The offers below were checked on October 12, 2026. Providers change them, so check the provider's pricing page (linked on each destination's settings card) before you rely on one.

| Destination | Free storage | How long |
|---|---|---|
| Google Drive | 15 GB per Google account, shared with Gmail and Google Photos | Always |
| Cloudflare R2 | 10 GB (Standard storage class), 1 million write and 10 million read requests a month. Downloads are never charged, so restores are free. | Always, every month |
| Backblaze B2 | The first 10 GB. Downloads are free up to three times what you store each month. | Always |
| OneDrive / Microsoft 365 | 5 GB per Microsoft account, shared with Outlook.com attachments | Always |
| Dropbox | 2 GB (Dropbox Basic) | Always |
| Microsoft Azure Blob Storage | 5 GB of hot blob storage (locally redundant), plus $200 of credit for the first 30 days, with a new Azure free account | 12 months |
| Amazon S3 | No free storage. New AWS accounts get $100 of credit (up to $200) on the free plan. | Up to 6 months |
| Wasabi (S3-compatible) | A trial with up to 1 TB, no credit card | 30 days |
| Scaleway (S3-compatible) | 750 GB for new Object Storage users | 90 days |
| DigitalOcean Spaces (S3-compatible) | None ($5 a month for 250 GiB) | – |
| SFTP, FTP / FTPS | Your own server or hosting account | – |
| Email | Limited by your mailbox and the attachment size limit | – |

To estimate what fits: backups are compressed, but images and videos barely shrink, so a full backup is usually between half and the whole size of your site's files plus the database. Each incremental backup only stores what changed. The Backups tab shows the size of every backup.

### Google Drive

"Store backups in Google Drive (personal or Google Workspace). You sign in with Google; no password is stored."

You create your own Google app once. Steps (from the field help):

1. In the Google Cloud Console, create a project (or choose one) and enable the **Google Drive API**.
2. Open **Google Auth platform** and set the audience:
   - Google Workspace: **Internal**.
   - Personal Google account: **External**, then **Publish app**. "In Testing mode Google ends the access after 7 days."
3. Go to **Clients → Create client**, choose **Web application**, and add the **Redirect URI** shown on the destination form as an authorized redirect URI. It must match exactly.
4. Copy the **Client ID** and the **Client secret** ("Shown when you create the client (Clients → your client → Client secrets).").
5. In WordPress, add a **Google Drive** destination, paste both, set the **Folder** and click **Save destination**.
6. Click **Connect Google Drive**, choose the account and allow access. You return with "Connected to Google Drive as you@gmail.com."

| Field | What to enter | Default |
|---|---|---|
| **Redirect URI** | Read-only, with **Copy**. Add it to your Google client. | – |
| **Client ID** (required) | Ends in `.apps.googleusercontent.com`. | Empty |
| **Client secret** (required) | Stored encrypted. | Empty |
| **Folder** | "Folder in your Google Drive for the backups, e.g. SymeloBackups. Use / for subfolders. The plugin creates it: it can only see folders and files it created itself, not ones you made by hand." | SymeloBackups |

Things to know:

- "The plugin only asks for access to the files it creates, so Google doesn't require an app review." It can't see the rest of your Drive.
- Because of that, files you upload or move into the folder by hand are invisible to the plugin. If you set up the destination on a new site and want to see older backups, use the same Google app (the same client ID).
- If you change the client ID after connecting: "The client ID changed after Google Drive was connected. Reconnect Google Drive on the Destinations tab."
- **Disconnect** also revokes the access at Google.
- The plugin refreshes sign-ins that haven't been used for 7 days, so they don't expire while unused.

### Dropbox

"Store backups in Dropbox, in an app folder or anywhere in your Dropbox. You sign in with Dropbox; no password is stored."

Steps (from the field help):

1. At dropbox.com/developers/apps, click **Create app**, choose **Scoped access**, then **App folder** (backups go to `Apps/<app name>`) or **Full Dropbox**.
2. On the **Permissions** tab, turn on **files.metadata.read**, **files.content.write** and **files.content.read**, and click **Submit**.
3. On the **Settings** tab, add the **Redirect URI** from the destination form, then copy the **App key**.
4. In WordPress, add a **Dropbox** destination, paste the **App key**, and click **Save destination**.
5. Click **Connect Dropbox** and allow access.

| Field | What to enter | Default |
|---|---|---|
| **App key** (required) | From the app's Settings tab. | Empty |
| **App secret** | "Optional: sign-in uses PKCE, which doesn't need the app secret. If you enter it, it's stored encrypted." | Empty |
| **Folder** | "Folder in your Dropbox for the backups, e.g. SymeloBackups. Use / for subfolders. With an "App folder" app it's inside Apps/<app name>/." | SymeloBackups |

If a permission is missing when you connect: "Dropbox didn't grant every permission backups need (missing: …). Check the app's permissions, click "Connect" again and allow access to files." Permissions changed after connecting only take effect after connecting again. **Disconnect** also revokes the access at Dropbox.

### OneDrive / Microsoft 365

"Store backups in OneDrive or OneDrive for Business (Microsoft 365). You sign in with Microsoft; no password is stored."

Steps (from the field help):

1. In the Microsoft Entra admin center (entra.microsoft.com), go to **App registrations → New registration**. Choose who may sign in ("for a personal OneDrive, include personal Microsoft accounts").
2. Add the **Redirect URI** from the destination form with the platform **Web**.
3. Under **API permissions → Add a permission → Microsoft Graph → Delegated permissions**, add **Files.ReadWrite**, **offline_access** and **User.Read**.
4. Under **Certificates & secrets → New client secret**, create a secret and copy its **Value** (not the Secret ID).
5. In WordPress, add a **OneDrive / Microsoft 365** destination, fill in the fields below and click **Save destination**.
6. Click **Connect OneDrive**, choose the account and accept.

| Field | What to enter | Default |
|---|---|---|
| **Application (client) ID** (required) | From the app's Overview page. | Empty |
| **Client secret** (required) | The secret's Value. "Secrets expire (after 24 months at most): add a new one before then." | Empty |
| **Tenant** (required) | "Depends on the app's "Supported account types" (on its Authentication page). Single tenant, the default for new apps: the Directory (tenant) ID from the app's Overview page. Multiple Entra ID tenants (multitenant): organizations. Multitenant and personal Microsoft accounts: common. Personal Microsoft accounts only: consumers." | Empty (placeholder "Directory (tenant) ID") |
| **Folder** | "Folder in your OneDrive for the backups, e.g. SymeloBackups. Use / for subfolders. It is created when needed." | SymeloBackups |

The **Tenant** must match the "Supported account types" you chose for the app. New apps are single-tenant: they need your **Directory (tenant) ID** from the app's Overview page; `common` fails with AADSTS50194. For a personal OneDrive, the app must allow personal Microsoft accounts. Other values are refused when you save. When the client secret expires, backups to OneDrive fail with a sign-in error: create a new secret, enter it in the destination, save, and connect again. Microsoft has no simple way to revoke a sign-in from outside, so **Disconnect** deletes the stored sign-in from your site.

### Sign-in problems (Google Drive, Dropbox, OneDrive)

| Message | Fix |
|---|---|
| "The sign-in link expired or was already used. Click "Connect" again." | Finish signing in within 10 minutes, in the same browser and admin account. |
| "Signing in to … didn't finish: …" | The provider's reason follows. Often the redirect URI doesn't match: copy the **Redirect URI** again and paste it into the app exactly. |
| "… Microsoft needs this app's own tenant. Copy the Directory (tenant) ID from the app's Overview page in Microsoft Entra into the destination's Tenant field, save, and click "Connect" again." | Microsoft answered AADSTS50194 (a single-tenant app used with `common`) or AADSTS90002 (unknown tenant). As the message says. |
| "The saved sign-in can't be read anymore (the site's security keys changed). Connect again." | The security keys in wp-config.php or `SYMELO_BACKUPS_KEY` changed, or the database came from another site. Click **Connect** again. |
| "… didn't send a refresh token, so backups would stop working within hours. Remove this app's access in your account's security settings, then click "Connect" again." | As the message says. |
| "… didn't accept the client ID or client secret. Check both on the destination (client secrets can expire), save, and connect again." | As the message says. |
| "Access to … was revoked or has expired. Reconnect … on the Destinations tab." | Open the destination and click **Reconnect**. |
| "This site uses plain permalinks, so the redirect URI contains "?", which sign-in services may reject." | Choose another structure under **Settings → Permalinks** first, then copy the new redirect URI. |

If your site's address changes (a new domain, or http to https), the redirect URI changes too. Update it in your app and connect again.

### Amazon S3 and S3-compatible storage

"Amazon S3, Wasabi, DigitalOcean Spaces, Scaleway, MinIO and other S3-compatible storage. Cloudflare R2 has its own destination."

| Field | What to enter | Default |
|---|---|---|
| **Provider** | **Amazon S3**, **Wasabi**, **DigitalOcean Spaces**, **Scaleway Object Storage**, or **MinIO or another S3-compatible service (custom endpoint)**. | Amazon S3 |
| **Access key ID** (required) | "Create a key that may only list, read, write and delete objects in this bucket (for AWS: s3:ListBucket, s3:GetObject, s3:PutObject, s3:DeleteObject and s3:AbortMultipartUpload)." | Empty |
| **Secret access key** (required) | Stored encrypted. | Empty |
| **Bucket** (required) | "The bucket must exist already. Keep it private." | Empty |
| **Region** | "Where the bucket is. Amazon S3 and Wasabi: for example us-east-1 or eu-central-1. DigitalOcean Spaces: for example nyc3, ams3 or fra1. Scaleway: fr-par, nl-ams, pl-waw or it-mil. Other services: as they document it, usually us-east-1." Required for Wasabi, DigitalOcean Spaces and Scaleway. | Empty |
| **Endpoint URL** | Only for a custom endpoint (required there). "The service's S3 API address, starting with https://, with the port if it isn't the default (for MinIO often https://your-server:9000). Plain http:// only works for a server in your own network (localhost, a private IP address or a name without dots), because backups would travel unencrypted." Other `http://` addresses are refused when you save. | Empty |
| **Path-style URLs** | "Puts the bucket name in the path (https://endpoint/bucket/…) instead of the host name. Always used for custom endpoints and bucket names with dots." | Off |
| **Storage class** | Only for Amazon S3: **Standard**, **Standard-Infrequent Access**, **One Zone-Infrequent Access**, **Intelligent-Tiering**, **Glacier Instant Retrieval**, **Glacier Flexible Retrieval**, **Glacier Deep Archive**. | Standard |
| **Server-side encryption** | Only for Amazon S3: "Ask Amazon S3 to encrypt the files at rest with SSE-S3 (AES-256). New buckets do this by default; turn it on when a bucket policy requires the header." | Off |
| **Folder** | See [Adding a destination](#adding-a-destination). | SymeloBackups |

Things to know:

- About storage classes: "Glacier Flexible Retrieval and Deep Archive are the cheapest, but their files must be restored in the AWS console (hours) before this plugin can download them, and early deletion is charged. Infrequent Access and Glacier classes charge minimum storage periods." Retention deletes old backups, so minimum storage periods may cost extra. For quick restores, use Standard, Intelligent-Tiering or Glacier Instant Retrieval.
- Files over 8 MB are uploaded in parts and resume where they stopped. A cancelled upload is aborted. An unfinished multipart upload can also be cleaned up by a bucket lifecycle rule.
- Avoid lifecycle rules that delete or move backup files. The plugin's retention already removes old backups, and rules that delete files can break incremental chains.
- Every request is signed (AWS Signature Version 4) and carries a checksum, so the storage rejects anything damaged on the way.
- Server-side encryption protects the files at the provider. For end-to-end protection, also turn on the plugin's encryption.

### Cloudflare R2

"Object storage from Cloudflare with no download fees, through its S3-compatible API." R2 includes 10 GB of storage free every month (see [Free storage at a glance](#free-storage-at-a-glance)).

Set it up in the Cloudflare dashboard:

1. Open **R2** (you may need to turn R2 on for your account once). Create a bucket, for example `site-backups`. Leave public access off. Keep the default storage class **Standard**: the free tier doesn't cover Infrequent Access.
2. Copy your **Account ID** from the R2 overview page (Account details).
3. Open **Manage API tokens** and create an R2 API token with the **Object Read & Write** permission, limited to this bucket. Copy the **Access Key ID** and **Secret Access Key**: Cloudflare shows the secret only once.

| Field | What to enter | Default |
|---|---|---|
| **Account ID** (required) | "Shown in the Cloudflare dashboard on the R2 overview page (Account details), 32 letters and digits." | Empty |
| **Access key ID** (required) | "In the Cloudflare dashboard, open R2, then Manage API tokens, and create an R2 API token with the "Object Read & Write" permission, limited to this bucket. Copy the Access Key ID and Secret Access Key it shows (only once)." | Empty |
| **Secret access key** (required) | Stored encrypted. | Empty |
| **Bucket** (required) | "A bucket you created in R2. Leave public access off." | Empty |
| **Jurisdiction** | **Default (automatic location)** or **European Union (EU)**. "Choose EU only for buckets created with the EU jurisdiction, which keeps the data inside the European Union." | Default |
| **Folder** | See [Adding a destination](#adding-a-destination). | SymeloBackups |

Things to know:

- A wrong account ID gives: "Enter your Cloudflare account ID (32 letters and digits)."
- R2 always encrypts stored files, so there is no server-side encryption option. For end-to-end protection, turn on the plugin's encryption.
- Downloads from R2 are free, so restoring or copying a backup back to the server costs nothing.
- Uploads over 8 MB go in parts and resume where they stopped, like Amazon S3.

### Backblaze B2

"Low-cost cloud storage from Backblaze, through its S3-compatible API."

| Field | What to enter | Default |
|---|---|---|
| **Application key ID** (required) | "Create an application key in Backblaze under Application Keys, limited to this bucket with read and write access. The master application key doesn't work with this connection." | Empty |
| **Application key** (required) | Shown once when you create the key. Stored encrypted. | Empty |
| **Bucket** (required) | "A private bucket that already exists." | Empty |
| **Endpoint region** (required) | "The part between "s3." and ".backblazeb2.com" in the bucket's endpoint, shown on the Buckets page (for example us-west-004 or eu-central-003)." | Empty |
| **Server-side encryption** | "Ask Backblaze to encrypt the files at rest (SSE-B2). Buckets can also do this by default." | Off |
| **Folder** | See [Adding a destination](#adding-a-destination). | SymeloBackups |

A wrong region gives: "Enter the endpoint region as shown on the Backblaze Buckets page, for example us-west-004."

### Microsoft Azure Blob Storage

"Store backups in a container of an Azure storage account."

| Field | What to enter | Default |
|---|---|---|
| **Storage account name** (required) | "The name of the storage account (3 to 24 lowercase letters and digits)." | Empty |
| **Sign in with** | **Access key** or **Shared access signature (SAS token)**. "A SAS token limited to one container is safer than the account's access key." | Access key |
| **Access key** | "In the Azure portal: storage account → Security + networking → Access keys." Copy the Key value, not the connection string. | Empty |
| **SAS token** | "Create it for this container (or for the Blob service with container and object access) with the Read, Write, Delete and List permissions, HTTPS only, and a far-away expiry date. Paste the part after the "?"." | Empty |
| **Container** (required) | "A private container that already exists." 3 to 63 lowercase letters, digits and single hyphens. | Empty |
| **Azure cloud** | **Azure (global)**, **Azure operated by 21Vianet (China)** or **Azure Government (US)**. | Azure (global) |
| **Access tier** | **Account default**, **Hot**, **Cool** (30-day minimum) or **Cold** (90-day minimum). "The Archive tier isn't offered: archived backups can't be restored without rehydrating them first." | Account default |
| **Folder** | See [Adding a destination](#adding-a-destination). | SymeloBackups |

When a SAS token expires, uploads fail. Create a new token and enter it in the destination.

### SFTP

"Any server you can reach over SSH: a storage box, a VPS or a NAS. Encrypted, with the server's identity checked."

| Field | What to enter | Default |
|---|---|---|
| **Server** (required) | For example `backup.example.com` or an IP address, without `sftp://`. | Empty |
| **Port** | 1 to 65535. | 22 |
| **Username** (required) | The SSH user. | Empty |
| **Sign in with** | **Password** or **Private key**. "A private key that is used only for backups is the safest choice." | Password |
| **Password** | For password sign-in. Stored encrypted. | Empty |
| **Private key** | "Paste the whole private key (OpenSSH, PEM or PuTTY format). Add its public key to ~/.ssh/authorized_keys on the server." Stored encrypted. | Empty |
| **Key passphrase** | "Only if the private key is protected with a passphrase." | Empty |
| **Host key fingerprint** | The server's SHA-256 fingerprint, like `SHA256:…`. See below. | Empty |
| **Folder** | "Folder for the backups, relative to the folder you land in after logging in (usually your home folder). Start with / for an absolute path. Each site gets a subfolder." | SymeloBackups |

**Trusting the server's host key.** The plugin only sends your password or key to a server whose identity it has checked. It never transfers backups without a saved fingerprint.

1. Save the destination with **Host key fingerprint** empty. The plugin connects without logging in and reports: "The server sent its host key. Check it below and click "Trust this key" if it is the right one."
2. The **Check the server's host key** card shows: "The server answered with a ssh-ed25519 key with this fingerprint: SHA256:…"
3. Compare it with the fingerprint your host gives you. On a computer you trust, you can also run:
   ```sh
   ssh-keyscan backup.example.com | ssh-keygen -lf -
   ```
4. Only if they match, click **Trust this key** within 15 minutes. The plugin saves it, logs in for real and tests: "The host key is saved and the connection works. The plugin now refuses to connect if the server shows a different key."

You can also paste the fingerprint into the field yourself before saving.

If the server's key changes later (a reinstall, a move, or an attack), backups to it stop: "The server's host key doesn't match the saved fingerprint (saved: …, server: …), so the plugin didn't log in. If the server was reinstalled or moved, check the new fingerprint with your host and save it. Otherwise someone may be intercepting the connection."

Connections time out after 30 seconds.

### FTP / FTPS

"A web host or NAS that offers FTP with TLS encryption (FTPS). Prefer SFTP when the server has it."

| Field | What to enter | Default |
|---|---|---|
| **Server** (required) | For example `ftp.example.com`, without `ftp://` or a folder. | Empty |
| **Port** | 1 to 65535. | 21 |
| **Username** (required) | | Empty |
| **Password** (required) | Stored encrypted. | Empty |
| **Encryption** | **Require TLS (explicit FTPS)** or **None: plain FTP (not recommended)**. "With plain FTP, your password and the backups travel unencrypted and can be read on the way. Only choose it for a server in your own network." | Require TLS |
| **Passive mode** | "Leave this on unless your host tells you otherwise; it works through most firewalls." | On |
| **Folder** | "Folder for the backups, relative to the folder you land in after logging in. Start with / for an absolute path. Each site gets a subfolder. Keep it outside the public web folder." | SymeloBackups |

Things to know:

- Only explicit FTPS (AUTH TLS, usually port 21) works. Implicit FTPS on port 990 isn't supported by PHP.
- If the server refuses TLS, the login fails; the plugin never falls back to plain FTP by itself.
- PHP doesn't check the FTPS server's certificate, so FTPS protects against eavesdropping but not against an impostor server. SFTP with a trusted host key is stronger.
- FTPS needs PHP's FTP extension built with OpenSSL: "This server's PHP can't use FTPS (its FTP extension was built without OpenSSL). Use SFTP instead."
- Uploads resume where they stopped when the server supports it.

### Email

"Emails each backup file as an attachment. Only for small sites: mail servers limit the size of attachments."

| Field | What to enter | Default |
|---|---|---|
| **Send to** (required) | "One or more addresses, separated by commas. Backups contain your whole site, including user data: send them only to mailboxes you trust." Up to 10. | Empty |
| **Largest file (MB)** | 1 to 100. "Files above this size aren't emailed. Attachments grow by about a third when sent, and most mail services accept 20 to 25 MB per email in total. Lower the archive split size in the plugin settings so that the parts fit." | 20 |

Things to know:

- Each file of a backup is sent in its own email, the manifest last. Subjects look like "Backup of My Site: 20261012-030000-full-a1b2c3 (files, part 2)".
- Emails go through `wp_mail()`, so the site's email setup must work (Symelo SMTP helps).
- A file over the limit isn't sent: "… is …, more than the email limit of …, so it wasn't emailed. Send this site's backups to cloud storage or SFTP instead, or lower the archive split size in the plugin settings." Set **Settings → Advanced → Split archives into parts of** below the limit, for example 12 MB for a 20 MB limit.
- The plugin can't list, download or delete emails. A backup that is only stored by email has no **Restore** or **Copy to this server** action; the list says "Stored by email only: download the attachments and upload them on the Migration tab to restore."
- To restore from emails: save every attachment of the backup into one folder, upload them under **Migration → Upload from your computer**, then **Import**. An import replaces the whole site except WordPress core files and wp-config.php.
- **Test** sends "Test email from Symelo Backups on …" with a small attachment, `symelo-test.txt`.

---

## Restoring

There are three ways to restore:

| Situation | Use |
|---|---|
| The site works and you can log in | [Restore from the dashboard](#restore-from-the-dashboard) |
| WordPress is broken (white screen, fatal error, hacked) but the server and its backups are there | [The recovery link](#when-the-site-is-broken-the-recovery-link) |
| The server is gone, or you're setting up a site on an empty server | [installer.php](#on-a-new-server-installerphp), or a fresh WordPress with this plugin |

Make a backup of the current state first when you can: if the restore isn't what you wanted, you can go back.

### Restore from the dashboard

1. On the **Backups** tab, click **Restore** next to the backup.
2. The **Restore the backup from …** form opens:
   - A yellow box: "Your site will be in maintenance mode during the restore. Visitors see a short "back in a moment" message. Most restores take a few minutes; large sites take longer. Keep this browser tab open until it says it's done."
   - For a backup from another site, a red box: "This backup was made by another site (…). Backups are signed by the site that made them, and this signature can't be checked here, so there is no way to tell whether the backup was changed. Only restore it if you trust where it came from."
3. Under **What to restore**, untick the parts you want to keep as they are. Each part says what it replaces (table below).
4. Optional settings:
   - **Remove plugins and themes that aren't in the backup** (when the backup contains plugins or themes): "Leave this off to keep plugins and themes installed after the backup was made." It isn't offered for backups before updates, which hold a single plugin or theme.
   - "Links are changed from https://old.example.com to https://example.com." when the backup was made at another address.
   - "This incremental backup builds on 3 earlier backups, which are used automatically."
   - **Restore from**: when the backup is in several places it can be restored from, choose which copy to use. This server is fastest. (Email copies can't be restored from.)
   - **Encryption password**, for encrypted backups: "Leave this blank to use the password saved in Settings. Enter a password only if this backup was made with an older one." It's required when no password is saved on this site any more, or the backup comes from another site.
   - For a backup from another site, tick "I trust this backup and want to replace this site with it."
5. Click **Restore now** and confirm "Restore this backup now? The parts you selected are replaced."

| Part | What restoring it does |
|---|---|
| **Database** | "Replaces all posts, pages, comments, users, orders and settings with their state on …. Anything added or changed since then is lost, and you may have to log in again." |
| **Plugins** | "Replaces the plugins in wp-content/plugins with the versions in the backup." |
| **Themes** | "Replaces the themes in wp-content/themes with the versions in the backup." |
| **Media uploads** | "Puts back the media files from the backup. Files uploaded since then are kept." |
| **Other content** | "Replaces the other files in wp-content, like must-use plugins and translations." |
| **WordPress core** | "Replaces wp-admin, wp-includes and the files in the main folder. wp-config.php is never overwritten." |

**Preparing.** First a job gets the backup ready while your site stays online: "Getting the backup ready. The restore starts as soon as it has been checked." It reads the backup's description and those of the backups it builds on, downloads the needed files from the destination if you restore from one (this needs disk space for the whole chain), checks every file's SHA-256 checksum, the signature and the encryption password. Errors stop it here, before anything changes, for example "The encryption password is wrong. Enter the password this backup was made with." or "An earlier backup that this incremental backup builds on is missing, so it can't be restored." A backup whose database doesn't fit this site is refused too: a multisite backup on a single site, a single-site backup on a network, or a network backup from another domain or path (see [Multisite](#multisite)).

**The restore page.** When everything is ready, your browser opens the restore page by itself. If it doesn't (for example because you closed the tab), the job's row under **Recent activity** says "Ready to restore. The link works until …" and has a **Continue to the restore** button; the page you started from shows "The backup is ready to restore." with the same button: "The link works for one hour. Your site is in maintenance mode while the restore runs." Only the administrator who started the restore sees the button. After the hour, start the restore again.

The restore page (title **Restore**) shows:

- **Backup**: **Backup from**, **Site**, **Contains**, **Size**, **Type** (for example "incremental, 4 backups in a row"), **Note**, **Encrypted**.
- **Server check**: **PHP**, **PHP extensions**, **Backup files**, **Encryption**, **Database** (a test table is created and dropped), **Folders** (write permission), **Disk space**, **Backup signature** (only for unverified backups, with a confirmation) and **Time limit**. Problems block the start; warnings need your confirmation.
- "Nothing has been changed yet. The restore replaces the parts listed above with the backup."
- Buttons: **Start the restore**, **Check again**, and **Cancel and go back to WordPress** (removes the restore tool; nothing was changed).

Click **Start the restore**. The page shows **Restoring** with a progress bar and the steps (see [What happens during a restore](#what-happens-during-a-restore)). "Keep this page open until it has finished. If the connection drops, open the same link again: the work continues where it stopped."

When it's done: "Your site has been restored. The backup has been restored and maintenance mode is off." with notes such as "If WordPress asks you to log in, use your login from the time of the backup." and a **Go to your dashboard** button. The restore tool deletes itself.

A restore link must be started within an hour. Once started, the same link can continue a stopped restore for two days.

### When the site is broken: the recovery link

You need a recovery link created beforehand (**Recovery** tab) and at least one complete backup on this server.

1. Open the recovery link. The page **Site recovery** lists the backups on this server, newest first (up to 200): date, type and note, what they contain and their size. Badges: **Verified** (signed by this site) or **Not verified**, **Encrypted**, **From …** (made at another address), **Files missing**.
2. Click **Restore…** next to the backup you want.
3. Under **What to restore**, choose the parts. **WordPress core files** is unticked by default. Optionally tick **Also remove plugin and theme files that aren't in the backup**. If the backup is encrypted and the link doesn't know the password, enter the **Encryption password**. Click **Continue** (or **Back to the list**).
4. The **Server check** runs, as for a dashboard restore. Click **Start the restore** (or **Choose another backup**).
5. The restore first checks every backup file's checksum (**Checking the backup files**), then restores.
6. When it's done: "Your site has been restored." and "Your recovery link keeps working, so keep it somewhere safe."

Things to know:

- The link only sees backups in this server's backup folder. Packages in the import folder aren't listed.
- The link knows the encryption password that was set when it was last updated, so you usually don't need to type it.
- A backup made at another address gets this site's address.
- If you enter a wrong link too often, the page is locked for 15 minutes. See [The restore or installer page is locked](#the-restore-or-installer-page-is-locked).

### On a new server: installer.php

`installer.php` sets up a migration package on any server, even one without WordPress. You need the package files, `installer.php` and the installer key. See [Move to a new host](#move-to-a-new-host) for the complete steps.

If you only have regular backups at a destination (no migration package), install WordPress and this plugin on the new server at the **same address** as before, add the destination with the same settings, click **Look for backups at destinations** on the Backups tab, and restore. The new installation can't verify the old signatures, so the backups show as from another site and you confirm that you trust them. If the address is different, open **Look for backups at destinations** and enter the old address's folder (see [After a domain change](#after-a-domain-change)), or download the backup's files from the destination and import them under **Migration → Upload from your computer**.

### What happens during a restore

The restore tool works through these steps, in short slices, so PHP's time limit doesn't matter:

| Step | What happens |
|---|---|
| **Checking the backup files** | Recovery link and installer only (the dashboard already checked them): every file's SHA-256 checksum is compared with the manifest. |
| **Turning on maintenance mode** | A `.maintenance` file is written. "Visitors see a short "briefly unavailable" message." It's refreshed while the restore runs. |
| **Restoring files** (installer: **Unpacking the files**) | The archives of every backup in the chain are extracted in order, full backup first. After each backup, files it recorded as deleted are removed. Only the chosen parts are written. |
| **Removing files that aren't in the backup** | Only with the "remove plugins and themes" option: files and folders in wp-content/plugins and wp-content/themes that the backup doesn't list are deleted. |
| **Restoring the database** (installer: **Importing the database**) | The newest backup's database dump is imported table by table. With a different table prefix, the tables are renamed, and the user roles and user settings are renamed too. |
| **Updating addresses in the database** | Only when the address or folder changed: old addresses and folder paths are replaced (see [Address replacement](#address-replacement)). |
| **Finishing up** | The installer writes wp-config.php. When the database was restored, the stored permalink rules are deleted (WordPress rebuilds them on the first visit). Maintenance mode is turned off. The restore tool removes itself (dashboard), resets itself (recovery link), or deletes the installer and package files (installer, when chosen). |

The restore never extracts files outside the WordPress folder: absolute paths, `..` and symbolic links in an archive are refused.

**If a step fails**, the page shows "This step stopped: …" with the reason and a hint, and two buttons: **Try again** continues where it stopped; **Start this step over** begins the step from scratch. Click **Copy the log** under **Log** when you contact support. The site stays in maintenance mode while the restore is paused. WordPress itself ignores a maintenance file that is more than 10 minutes old, so visitors see the site again after a while, possibly half-restored: finish or repeat the restore.

### What a restore doesn't change

- **wp-config.php** is never overwritten, so a restore can't lock you out of your database. The copy in the backup is only used by the installer.
- Files created after the backup stay in place: media uploaded since, and plugins and themes added since (unless you tick the "remove plugins and themes" option).
- Database tables that aren't in the backup (for example from a plugin installed later) stay.
- The plugin's own backup folder, the restore and recovery tools and `.maintenance` are never touched.
- Folders in the WordPress folder other than wp-admin, wp-includes and wp-content aren't in backups, so they aren't changed.
- Restoring the database also restores this plugin's own settings, schedules and destinations as they were then. Your list of backups isn't affected, because backups are files, not database rows.

---

## Migrating and cloning

A migration package reaches the new site in one of three ways:

| Way | When to use it |
|---|---|
| **installer.php** | The new server is empty (no WordPress yet). |
| **Pull from another site** (migration link) | WordPress and this plugin are installed on the new site, and the new site can reach the old one over the internet. Fastest: server to server. |
| **Upload from your computer** or **Files uploaded by FTP** | WordPress and this plugin are installed on the new site, but it can't reach the old one (or you have the files already). |

### Creating the package

On the old site: **Migration → Create migration package**. Save the installer key right away. See [Export this site](#export-this-site).

A package contains every component, WordPress core included, and wp-config.php. It is encrypted when encryption is on in the old site's settings; then you need that password on the new site.

### Importing into an existing WordPress site

1. On the new site, install WordPress (if needed) and this plugin.
2. Get the package there: paste the migration link under **Pull from another site** and click **Download package**, or upload the files.
3. Under **Packages ready to import**, click **Import**, enter the encryption password if needed, tick the confirmation and click **Import and replace this site**.
4. Follow the restore page to the end.
5. Log in with a username and password from the old site.

An import replaces the database, plugins, themes, media and other content. It never touches WordPress core files or wp-config.php: the new site keeps its own database login and security keys. Use the same or a newer WordPress version on the new site as on the old one. When the package was made with a newer WordPress, the import form shows a warning before you start ("This package was made with WordPress …, and this site runs …. An import never replaces WordPress itself, so update WordPress here first, then import."), and preparing the import adds this warning to the job's log: "This package was made with WordPress …, but this site runs WordPress …. An import doesn't change WordPress itself, so plugins, themes and the database from the newer version may not work here. Update WordPress on this site first (Dashboard → Updates), then import." The import isn't stopped, so check the version before you import.

### Using the installer

1. Create an empty database on the new server, and a database user with all privileges on it.
2. Upload `installer.php` and all package files to the folder where the site should live (usually the web root). The package files can also be in a subfolder named after the backup ID.
3. Open `https://your-new-site/installer.php`. The page **Site installer** asks: **Enter your installer key** ("You saw this key when you created the migration package on the old site."). Wrong keys: "That key isn't right. 9 tries left before a 15-minute pause."
4. Fill in **The new site**: **Database host** ("Often localhost."), **Database name**, **Database user**, **Database password**, **Table prefix** (filled in with the old site's prefix), **Site address** ("Where the new site will be. We filled in the address of this page."), and the **Encryption password** when the package is encrypted. Keep **Delete the installer and the backup files when the site is ready (recommended)** ticked. Click **Check and continue**.
5. The **Server check** adds two checks: **WordPress** (the core files are in the package or already in the folder) and **Existing site**. If the folder already has a wp-config.php, or the database has tables with this prefix: "There is already a site here: …. Installing replaces it. Back it up first if you still need it." Tick "Yes, replace the existing site" to continue.
6. Click **Install the site**. The installer checks every file, unpacks the files, imports the database, replaces the addresses and writes wp-config.php: the old site's file with the new database details (or a new one with fresh security keys when the old one isn't in the package).
7. "Your site is ready. The files are in place, the database is imported and wp-config.php is written." Click **Log in to your site** and log in with a username and password of the old site.

The installer keeps its working files in a folder `symelo-installer-data` next to it. Once it has finished, it refuses to run again: "This installer has finished. The site has been set up. For your security the installer won't run again." Check that `installer.php`, the package files and that folder are gone; delete them by hand if a note says they couldn't be removed.

The installer needs PHP 7.4+, zlib, mysqli, and sodium or openssl. An encrypted package that was made where PHP has the sodium extension can only be opened where PHP has it too.

### Address replacement

When the backup or package was made at another address, the old address is replaced with the new one throughout the database:

- `http://` and `https://` forms, addresses without a scheme (`//old.example.com`), JSON-escaped (`https:\/\/old.example.com`) and URL-encoded forms;
- inside serialized data (as many plugins store their options), with the string lengths corrected;
- without changing longer addresses that merely start the same way (`old.example.com.au` stays as it is);
- the WordPress address (siteurl) too when it differs from the site address, and the server folder path when the site moves to another folder.

Post GUIDs are left alone.

On a dashboard restore of your own backup, addresses are only replaced when the backup's address differs from this site's address.

### After a migration

- You log in with the old site's usernames and passwords.
- This plugin's settings, schedules and destinations come with the database. With an import, the new site keeps its own wp-config.php security keys, so saved passwords, keys and sign-ins of the old site can't be decrypted: destinations show **Needs attention** or **Reconnect needed** ("The saved sign-in can't be read anymore (the site's security keys changed). Connect again."). Enter the values again and connect again, or remove the destinations you don't want here. Set the encryption password again. (With the installer, the old site's wp-config.php comes along, so they keep working. If both sites define the same `SYMELO_BACKUPS_KEY`, they keep working too.)
- Check the **Schedules** tab: a clone runs the same schedules as the original, to the same destinations.
- Backups made on this server before the import may now show as from another site. See [Backups marked as from another site](#backups-marked-as-from-another-site).
- Create a new recovery link on the new site.

### Multisite

- A backup of a multisite network contains every site's tables and the whole uploads folder.
- To move a network to a new domain or path, use the **installer**. It updates the network's main domain and path in the database and in wp-config.php: "This is a multisite network moving to a new address. The installer updates the main domain and path of the network. Sites with their own domains keep them; check them in Network Admin afterwards."
- An import on the Migration tab, and a dashboard restore of a network backup's database, only work for a network at the same domain and path. Otherwise they're refused before anything changes, for example: "This network package was made at …, but this network is at …. An import can't move a network to another domain or path. Use the package's installer instead: it moves the network to its new address and writes a matching wp-config.php."
- A multisite package or backup can't go into a single site, and a single-site one can't go into a network. For example: "This package comes from a multisite network, but this site is a single site. Importing it here would break the site. To move the network, use the package's installer on a new server or in an empty folder."
- These checks only concern the database: restoring only files (plugins, themes, media, other content) is always possible.
- The plugin has no option to move a single site into a network, or one site out of a network: backups and packages always cover the whole network.

---

## Backups before updates

With **Settings → Before updates → Back up before updates** on (the default), the plugin makes a quick backup right before WordPress downloads an update:

| Update | What is backed up |
|---|---|
| A plugin | The database and that plugin's folder. |
| A theme | The database and that theme's folder. |
| WordPress | The database and WordPress core (wp-admin, wp-includes and the files in the main folder). |

Details:

- It works for updates you start and for automatic updates. New installs and translation updates are skipped.
- When several plugins are updated at once, the database is backed up once, with the first one.
- It runs in the same request as the update, within about 60 seconds in total. If it can't finish in time, or another backup is running, it's skipped and the update goes ahead. An update is never blocked. **Settings → Before updates** shows how the last one went.
- These backups stay on this server; they aren't copied to destinations. They follow the encryption setting.
- They appear in the list with the type **Pre-update** and a note like "Before updating the plugin Akismet". The newest 3 are kept (**Keep the newest … of these backups**); they don't count towards the other local backups.
- No success email is sent for them; failures are emailed like other backups.

**Undo a bad update:** on the **Backups** tab, click **Restore** next to the **Pre-update** backup. To put back only the old plugin version, tick **Plugins** and untick **Database**. Tick **Database** too if the update changed the database and the new version left it in a state the old one can't use.

---

## Encryption

### What is encrypted

| Setting | Encrypted | Not encrypted |
|---|---|---|
| **Encrypt the database** | The database file (`….database.sql.gz.enc`). | File archives, file index, manifest. |
| **Encrypt everything** | The database file, every file archive part and the file index (`.enc`). | The manifest. |

The manifest (`manifest.json`) is never encrypted, so the plugin can list and check backups without the password. It contains the site's name and address, the WordPress folder path, the table names with their row counts, the number and total size of files, the backup's checksums, and for incremental backups the paths of deleted files.

Files are encrypted on this server before they're uploaded, with XChaCha20-Poly1305. The key is made from your password with Argon2id when PHP has the sodium extension, or otherwise with PBKDF2-SHA256 and 600,000 rounds. Argon2id uses 64 MB of memory, or 256 MB when PHP's memory limit on the server that makes the backup is unlimited or at least 512 MB; the server that restores it needs as much. A wrong password is detected before anything is decrypted, and any change to an encrypted file is detected.

### The password

- The password is saved in the database, encrypted (see [Privacy and security](#privacy-and-security)), so scheduled backups can encrypt without asking.
- **If you lose the password, encrypted backups can't be restored by anyone.** Keep it in a password manager.
- You need it to restore a backup on another site or server, to import an encrypted migration package, and in the installer.
- A dashboard restore uses the saved password automatically. A restore of the database brings back the password that was saved at that time.
- The recovery link remembers the password too.

### Changing or removing the password

- Enter a **New password** and **Repeat it** under **Settings → Encryption → Change the password**, then **Save changes**: "Encryption password saved. Keep it somewhere safe: without it, encrypted backups can't be restored."
- "Changing it applies to new backups only: older ones still need the password they were made with." Keep the old password as long as you keep backups made with it. To restore one, enter its password in the **Encryption password** field of the restore form.
- With **Encrypt everything**, the next incremental backup after a password change becomes a full one.
- **Remove password** (in the **Remove the password** card) turns encryption off: "New backups are no longer encrypted. Existing encrypted backups keep needing the password, so don't forget it."
- Don't change the password while a backup runs: "The encryption password changed while the backup was running. Please start the backup again."

---

## Retention and disk space

### Where the rules apply

| Rule | Set in | Applies to |
|---|---|---|
| Local | **Settings → Retention**: keep the newest 5, no age limit (defaults) | Every backup on this server except backups before updates: manual backups, scheduled backups, migration packages and copies made with **Copy to this server**. |
| Before updates | **Settings → Before updates**: keep the newest 3 | Pre-update backups on this server. |
| Per schedule | **How many to keep** of each schedule: keep the newest 7 (default) | That schedule's backups at each of its destinations; on this server too when the schedule has no destination. Backups found in another site's folder (see [After a domain change](#after-a-domain-change)) are never removed by it. |

Rules run after each backup (the local rule, or for backups before updates their own rule, plus the schedule's rule for scheduled backups) and once a day for everything.

How the rules decide:

- A backup is removed when it's beyond **Keep the newest** or older than **delete them after … days**. 0 means no limit for that part.
- The newest backup a rule covers is always kept, so an age limit can't remove everything when backups stop for a while.
- **Chains are kept complete**: a backup that a remaining incremental backup at the same place builds on is never removed. With incremental schedules you may therefore see more backups than the number you set, until a new full backup starts a new chain.
- The backup that was just made is never removed by its own run.
- Backups you start yourself and copy to destinations aren't removed at destinations by any rule. Delete them on the Backups tab when you no longer need them.
- Migration packages count towards the local rule. Creating a package can push out an older local backup.
- Packages in the import folder aren't covered by any rule: delete them under **Migration → Packages ready to import**.
- Backups found in another site's folder at a destination (an older address, see [After a domain change](#after-a-domain-change)) are never removed automatically.

### Disk space

- Every backup is made on this server first. Expect roughly the compressed size of your site for a full backup, much less for incremental ones. Images and videos hardly compress.
- A backup doesn't start with less than 50 MB free. Before the database and file steps it estimates the space needed; it stops when there clearly isn't enough ("There isn't enough free disk space for the file archives: at least … is needed and only … is free. Free up space or remove old backups, then try again.") and warns when it's tight ("Disk space is tight: … may need up to … and … is free. The backup continues, but it may fail if the disk fills up.").
- Restoring from a destination downloads the needed files to this server first.
- Uploading or pulling a package needs its size plus 50 MB free.
- The free space is shown on the Destinations tab (**This server**), in **Settings → Retention** and in Site Health.

To free space: lower **Keep the newest** in **Settings → Retention**, delete old backups on the Backups tab, delete imported packages on the Migration tab, or exclude large folders you don't need (caches, old archives).

---

## How-to recipes

### Daily database, weekly full, to Google Drive and S3

1. Add a **Google Drive** and an **Amazon S3** destination and make sure both tests pass.
2. On **Schedules**, add a schedule:
   - **Name**: "Daily database", **How often**: **Every day**, **Time**: 02:00.
   - **What to back up**: only **Database**. (A database-only backup is always complete on its own.)
   - **Where to keep it**: both destinations.
   - **Keep the newest** 30 backups.
3. Add a second schedule:
   - **Name**: "Weekly full", **How often**: **Every week**, **Day of the week**: Sunday, **Time**: 03:30.
   - **Type**: **Full every time**.
   - **What to back up**: everything you need.
   - **Where to keep it**: both destinations. **Keep the newest** 8 backups.
4. In **Settings → Retention**, set how many stay on this server, for example 7.

Both schedules also leave their backups on this server, where the local rule applies.

### Move to a new host

**The new server is empty:**

1. On the old site: **Migration → Create migration package**. Save the installer key.
2. Download every package file and **Installer** (`installer.php`).
3. On the new host: create an empty database and user, then upload `installer.php` and the package files to the web root.
4. Open `installer.php` at the new site's address and follow [Using the installer](#using-the-installer). If you keep the same domain, enter it under **Site address**.
5. Point the domain to the new server if you haven't yet, log in and check the site.
6. On the new site: check destinations and schedules, and create a new recovery link.

**WordPress is already installed on the new host:**

1. Install this plugin there.
2. On the old site: **Create migration package** and copy the **Migration link**.
3. On the new site: **Migration → Pull from another site**, paste the link, **Download package**, then **Import**.

### Clone production to staging

1. On the staging site (WordPress and this plugin installed), protect it from sending email to customers. With Symelo SMTP, add `define( 'SYMELO_SMTP_DELIVERY', 'log_only' );` to its wp-config.php before the import.
2. On production: **Migration → Create migration package**, copy the migration link.
3. On staging: **Migration → Pull from another site**, paste the link, **Download package**, **Import**.
4. After the import, log in with your production account. On staging, open **Schedules** and turn off or delete the schedules you don't want there, and review **Destinations**. (They come from production; see [After a migration](#after-a-migration).)

If staging can't reach production over the internet (for example both run on the same private network), upload the package files instead, or ask a developer about the `symelo_backups_pull_allow_private_urls` filter.

### Restore one plugin

- **After a bad update**: restore the **Pre-update** backup with only **Plugins** ticked. It contains just that plugin. See [Backups before updates](#backups-before-updates).
- **From a regular backup**: restoring **Plugins** puts back every plugin as it was then, which also undoes newer updates of other plugins. To get just one plugin from an unencrypted full backup, download its **Files** parts, extract `wp-content/plugins/plugin-name/` with an archive tool, and upload that folder by FTP.

### Back up a small server gently

1. **Settings → Advanced → Speed**: **Gentle, for small servers**.
2. Lower **Compression** to 3 or 4: less CPU, slightly bigger files.
3. Leave out what you don't need: caches, old archives, `node_modules`.
4. Use an **Incremental** schedule at a quiet time, so most runs only save changed files.
5. If backups still stop with "The job stopped three times in a row because PHP ran out of memory/time…", see [Time and memory limits](#time-and-memory-limits).

### Keep backups mostly offsite

Every backup is made on this server first, and the plugin always keeps at least the newest one there (needed for quick restores and the recovery link).

1. Add at least one destination and tick it in your schedules.
2. In **Settings → Retention**, set **Keep the newest** to 1. (0 means no limit, not "none".)
3. Use **Full every time** schedules if you want only one backup on this server. With incremental schedules, the current chain stays on this server until a new full backup starts.

### Run backups from a system cron job

When your host blocks the site's requests to itself, or WP-Cron is turned off (`DISABLE_WP_CRON`), let the server's cron run the work. `wp symelo-backups run` starts the scheduled backups that are due, then works on the queued jobs until none are left.

For example, every 5 minutes:

```sh
*/5 * * * * cd /path/to/wordpress && wp symelo-backups run --max-seconds=270
```

The daily maintenance (retention, cleanup, keeping cloud sign-ins alive) still runs through WP-Cron. With WP-Cron off, add `wp cron event run --due-now --quiet` to the same cron job.

`--max-seconds` ends the run before the next cron job starts; the rest continues in the next run. `wp symelo-backups run` doesn't pause between steps and doesn't wait for a busy server.

Without WP-CLI, an external cron service can call `POST /wp-json/symelo-backups/v1/run` with an administrator's application password (see [REST API](#rest-api)). Each call starts the due schedules and works for up to 60 seconds.

---

## Troubleshooting

### Start here

1. On the **Backups** tab, look at **Recent activity** and open **View log** of the failed job. The last lines usually say exactly what went wrong.
2. Check **Tools → Site Health**.
3. On the **Destinations** tab, click **Test** for each destination.

### Jobs don't start or get stuck

The job stays at "Waiting to start…", or Site Health says "A backup job is stuck".

- Only one job runs at a time. "Waiting for the job before it to finish…" means another job is still running.
- Keep the **Backups** screen open for a minute: it runs steps itself. If the job moves now, the background start is blocked.
- Common causes: a firewall or security plugin blocking requests from the site to itself, HTTP authentication on a staging site, or WP-Cron turned off. **Site Health → Info → Symelo Backups and Migration → Background runner** shows when the runner last ran and any error sending its request.
- Fix: allow the site's requests to `wp-admin/admin-ajax.php`, or run the work from a system cron job (see [Run backups from a system cron job](#run-backups-from-a-system-cron-job)).
- A job you don't want any more: click **Cancel** on its progress card.
- Schedules don't run at all: check that WP-Cron runs. **Scheduler event** under **Site Health → Info → Symelo Backups and Migration** should say **Scheduled**. Without WP-Cron, `wp symelo-backups run` from a system cron job starts due schedules too.
- **Run now** says "A backup for this schedule is already running.": wait for that backup, or cancel it.

### Time and memory limits

The plugin works in short steps and saves its place after each one. When PHP is killed during a step, the next step continues from the last saved point with shorter steps. After three interruptions in a row, the job fails with one of these messages:

| Message | What to do |
|---|---|
| "The job stopped three times in a row because PHP ran out of memory. Ask your host to raise the PHP memory limit, or exclude very large files or folders in Settings, then try again." | As it says. Very large single files are the usual cause. |
| "The job stopped three times in a row because PHP ran out of time. Choose the speed "Gentle, for small servers" in Settings → Advanced (it works in shorter steps), or ask your host to raise max_execution_time, then try again." | As it says. |
| "The job stopped three times in a row before a step could finish (the server ended the request). Try the speed "Gentle, for small servers" in Settings → Advanced and check the PHP error log. Last error: …" | As it says; the last error helps your host. |

### Not enough disk space

| Message | What to do |
|---|---|
| "There is only … of free disk space on this server, which isn't enough to make a backup. Free up space or remove old backups first." | See [Disk space](#disk-space). |
| "There isn't enough free disk space for the database backup / the file archives: …" | Same. |
| "The archive part … wasn't written completely. The disk may be full." | Same. |

### Destination errors

| Message | Cause and fix |
|---|---|
| "Uploading to … failed: … The backup is still kept on this server." | The backup is fine; only the copy failed. The reason follows the colon. Fix the destination and test it; the next backup copies again. Temporary problems (rate limits, server errors, network trouble) are retried automatically first. |
| "… isn't connected yet. Open the destination on the Destinations tab and click "Connect …"." | Connect the account. |
| "Access to … was revoked or has expired. Reconnect … on the Destinations tab." | Click **Reconnect**. For OneDrive, also check that the client secret hasn't expired. |
| "The server's host key fingerprint isn't saved yet…" / "The server's host key doesn't match the saved fingerprint…" | SFTP. See [SFTP](#sftp). |
| "The server didn't accept the username and password." / "The server didn't accept the private key…" | SFTP login. Check the username and the key in `~/.ssh/authorized_keys`. |
| "Couldn't connect to … on port …. Check the server name and port, and that the server allows connections from this site's IP address." | The server isn't reachable from your host. Ask both sides about firewalls. |
| "Enter the endpoint region as shown on the Backblaze Buckets page, for example us-west-004." | B2 region. |
| "Request signing doesn't work on this server…" | S3: contact support. |
| "Endpoint URL: use https://. Plain http:// is only accepted for a server in your own network…" | S3-compatible storage with a custom endpoint: use the `https://` address of the service. |
| "Tenant: enter the Directory (tenant) ID from the app's Overview page…" | OneDrive: see [OneDrive / Microsoft 365](#onedrive--microsoft-365). |
| "… is …, more than the email limit of …, so it wasn't emailed…" | Email. See [Email](#email). |
| "The email couldn't be sent: … Check this site's email setup (for example with Symelo SMTP), and that the mail server accepts attachments of this size." | Email. Check the site's email setup. |
| "The destination "…" is turned off and was skipped." | Turn it on again, or untick it in the schedule. |
| "A saved password or key can't be decrypted any more (the site's security keys changed). Enter it again." | See [The saved value can't be decrypted any more](#the-saved-value-cant-be-decrypted-any-more). |

### Backups at a destination don't show up

- Click **Look for backups at destinations** on the Backups tab.
- By default the plugin only looks in this site's folder, which is named after the site's current address (for example `SymeloBackups/example.com/`). After a change of address, the older backups are in the old folder: see [After a domain change](#after-a-domain-change).
- Google Drive only shows files the plugin itself created, with the same Google app.
- Email can't be listed.
- Open the log of the **Destination check** job under **Recent activity**: "The backups on … could not be listed: …" names the problem.

### After a domain change

Backups at destinations are stored in a folder named after the site's address. After a move to a new domain (or a new path), new backups go to a new folder, and the older ones stay in the old one. To see them again:

1. On the Backups tab, open **Look for backups at destinations**.
2. Enter the old folder under **Site folder**, for example `old-domain.com`, or click **Show the folders at my destinations** and pick it.
3. Click **Look for backups**.

The backups found there are added to the list. They show as from another site, so restoring them asks you to confirm, and their old address is replaced with the current one. They're downloaded from the old folder, new incremental backups never build on them, and schedule retention never deletes them: delete them yourself when you don't need them any more.

A multisite network's database can't be restored at another address this way; use a migration package's installer (see [Multisite](#multisite)).

### Backups marked as from another site

The list shows "From https://…" and restoring asks you to confirm that you trust the backup. That happens when:

- the backup was made by another site (an import, or a destination folder shared by mistake);
- the backup was found in the destination folder of another address, for example this site's old domain (see [After a domain change](#after-a-domain-change));
- the backup's manifest was changed after it was made;
- this site's security keys changed since the backup was made: new WordPress salts in wp-config.php, adding or changing `SYMELO_BACKUPS_KEY`, or an import of another site's database. The plugin then can't read its signing key any more and creates a new one, so all older backups look foreign, even with this site's own address. The plugin tells you when this happens, and **Tools → Site Health** shows "Older backups show as made by another site".

If you know the backup is yours, tick the confirmation and restore it.

### Restore problems

| Message or situation | What to do |
|---|---|
| "This backup is encrypted. Enter the password it was made with." / "The encryption password is wrong…" | Enter the password that was set when the backup was made. |
| "An earlier backup that this incremental backup builds on is missing, so it can't be restored." | A chain member was deleted. Restore the full backup of the chain, or another chain. |
| "This file was encrypted with Argon2id, which needs the PHP sodium extension…" | Ask your host to enable the sodium extension. |
| "The restore tool could not be set up. Check that wp-content is writable." | The restore tool is copied to wp-content. Fix the folder permissions. |
| "The restorer could not be started: the server answers with an error (500). The PHP error log of your hosting account shows why." | The server refuses to run PHP in a new folder in wp-content. Ask your host. |
| A page **This restore link doesn't work**: "The link is wrong or incomplete, it has expired, or it was already used. A restore link has to be opened within an hour, and works for one restore only." | Restore links expire after an hour if not started, and after two days once started. Go back to the Backups tab and start the restore again; if WordPress doesn't load any more, use the recovery link. A plain "Not Found" page means the restore tool has already finished and removed itself. |
| A page **This recovery link doesn't work**: "The link is wrong or incomplete, or it was replaced by a newer link or turned off." | Copy the whole link again (it ends with a long code after `?key=`). If WordPress still works, create a new link under **Backups → Recovery**; otherwise use a migration package's installer, or ask your hosting provider. |
| "This link can't be used. It has expired, was already used, or isn't complete…" | Shown when the restore page was already open and the link stopped working. As for the pages above. On the recovery page the text says the recovery link was replaced or turned off: use the newest one from **Backups → Recovery**. |
| "This backup comes from a multisite network, but this site is a single site…" / "This backup comes from a single site, but this site is a multisite network…" / "This network backup was made at …, but this network is at …" | The database doesn't fit this site. See [Multisite](#multisite). Restoring only files is still possible. |
| "A statement in the database backup is larger than this database server accepts (max_allowed_packet is … MB)." | Ask your host to raise `max_allowed_packet` for MySQL/MariaDB. |
| "The database user can't create and delete tables: …" | Give the database user all privileges on the database. |
| "The web server can't write to …" | Fix the folder permissions, then click **Check again**. |
| The site still says "Briefly unavailable for scheduled maintenance" | The restore stopped. Open the restore link again and click **Try again**, or delete the `.maintenance` file in the WordPress folder. |

### The restore or installer page is locked

After 10 wrong keys or links from the same IP address within 15 minutes (or 100 from all addresses), the page refuses further tries for 15 minutes:

- The installer says "Too many wrong keys. For your security, try again in … minutes."
- Restore and recovery links open a page **Please wait a moment**: "This page was opened with a wrong link too often, so it is locked for a while to keep your site safe." It says how long to wait, and to copy the whole link.

Wait as long as the page says and use the correct link or key. Behind a proxy or CDN, all visitors may count as one address; a wrong guess by someone else can then lock you out too. If the restore tool can't keep its count of wrong tries (for example because its folder isn't writable), it never locks anyone out.

### Migration problems

| Message | What to do |
|---|---|
| "This isn't a valid migration link. Copy the whole link from the Migration tab of the old site (it starts with "symelo-migrate:")." | Copy the complete link again. |
| "This migration link has expired (links work for 48 hours). Click "New migration link" next to the package on the old site's Migration tab." | On the old site, open the package under **Migration → Packages on this server** and click **New migration link**. |
| "This migration link is from this site. Paste it into the site you are moving to." | Use the link on the new site. |
| "The other site refused the download…" | The link expired or was copied incompletely. Make a **New migration link** on the old site. |
| "The package isn't on the other site anymore…" | The package was deleted on the old site. Create a new package there. |
| "This package comes from a multisite network…" / "This package comes from a single site…" / "This network package was made at …" | The package doesn't fit this site. See [Multisite](#multisite). |
| "The package could not be downloaded from the other site: …" | Network problems; the pull retries several times first. Check that the new site can reach the old one, or upload the files instead. |
| "The server refused the upload because it is too large. Ask your host to raise upload_max_filesize and post_max_size, or upload the files by FTP." | As it says. |
| "Your login session expired. Reload the page and choose the files again: the upload continues where it stopped." | As it says. |
| "This package isn't complete yet. Missing: …" | Upload the missing files. |
| "This package builds on the backup …, which isn't on this server. Upload it too." | An incremental backup needs its whole chain: upload the files of every backup it builds on. |

### Recovery link problems

- "The recovery files are missing or can't be written…": open any wp-admin page (the plugin sets them up again), or click **Create a new link**.
- "The recovery link could not be set up: …": usually wp-content isn't writable.
- After a change of security keys (salts or `SYMELO_BACKUPS_KEY`), the plugin can't update the link any more. The old link still works, but create a new one so it uses the current settings.

### The saved value can't be decrypted any more

You see "The saved value can't be decrypted any more (the site's security keys changed). Enter it again." on a destination, destinations show **Needs attention**, or a Google Drive, Dropbox or OneDrive destination shows **Reconnect needed** with "The saved sign-in can't be read anymore (the site's security keys changed). Connect again."

Passwords, keys and sign-ins are encrypted with `SYMELO_BACKUPS_KEY` or, without it, with your site's security salts. If the key or the salts changed (some security plugins rotate salts; an import keeps the new site's salts), the old values can't be read. Enter them again, connect OAuth destinations again, and set the encryption password again. Defining a `SYMELO_BACKUPS_KEY` (and keeping it when you move the site) prevents this in future.

### Still stuck?

Go to the **Support** tab, click **Copy system information**, and email it with a description of the problem (and the job log) to office@symelo.com.

---

## Privacy and security

### What is stored, and where

| Data | Where |
|---|---|
| Backups | `wp-content/uploads/symelo-backups-{random}/backups/{backup ID}/` (on multisite, in the main site's uploads folder). |
| Job files, logs, temporary files | `…/jobs/`, `…/logs/`, `…/tmp/` in the same folder. Logs and jobs are deleted after 30 days, temporary files after a day. |
| Uploaded and pulled packages | `…/import/{backup ID}/`. |
| Recovery link configuration | `…/recovery/` and the recovery tool in `wp-content/symelo-recovery/`. |
| Restore tools | `wp-content/symelo-restore-{random}/`, while a restore is prepared or running. Removed when done, or after a day without use. |
| Settings, schedules, destinations, OAuth sign-ins, the list of backups at destinations | Options in the WordPress database. Passwords, keys, tokens, the encryption password and the signing key are encrypted. |

The storage folder's name has a random part, and the plugin puts `.htaccess`, `web.config` and `index.php` files in it that block web access on Apache, LiteSpeed and IIS. **Nginx ignores these files.** On Nginx, block the folder in the server configuration (your host can help):

```nginx
location ~ /symelo-backups- { deny all; }
```

Site Health checks that backups can't be downloaded from the web, whatever server you use. Downloads in the admin go through WordPress after a permission check; the storage folder is never linked.

The recovery and restore tools need the database login and the encryption password. They keep them encrypted with a key derived from the link's secret, which is never stored in a file. Access rules for Apache, LiteSpeed and IIS block everything in these folders except `index.php`; on Nginx the encryption protects them.

### What is sent where

- **Backups** go only to the destinations you set up, with the account details you entered: Google Drive (oauth2.googleapis.com, accounts.google.com, www.googleapis.com), Dropbox (www.dropbox.com, api.dropboxapi.com, content.dropboxapi.com), OneDrive (login.microsoftonline.com, graph.microsoft.com), Amazon S3 (s3.{region}.amazonaws.com), Wasabi, DigitalOcean Spaces, Scaleway, Cloudflare R2 ({account}.r2.cloudflarestorage.com), Backblaze B2 (s3.{region}.backblazeb2.com), Azure Blob Storage ({account}.blob.core.windows.net), your MinIO or other custom S3 endpoint, your SFTP or FTP server, or email through `wp_mail()`. Nothing is sent until you add a destination. Backups, file names and your site address (the name of the site's folder) are sent only as needed to store and list backups. Requests to S3-compatible storage, Cloudflare R2, B2 and Azure identify themselves with a `SymeloBackups/<version>` user agent that includes your site's address.
- **Background work**: the site sends short requests to itself (`wp-admin/admin-ajax.php`, authenticated with a random per-site token) and uses WP-Cron. Site Health requests a test file from the backup folder.
- **Migration links**: the new site downloads the package from the old site, with signed requests that expire with the link.
- **Notifications** go to your notification addresses.
- Nothing is sent to Symelo LTD.

### Personal data

Backups contain your whole database: user accounts, orders, comments and everything else. Keep that in mind for data protection:

- Store backups only where you are allowed to keep personal data, and limit how long you keep them with retention rules.
- Deleting a user or erasing personal data in WordPress doesn't change existing backups. They disappear when retention removes them.
- Encryption (**Encrypt everything**) makes stolen or leaked backups useless without the password.

### Security notes

- Every admin action checks permissions and WordPress's nonce protection. REST routes need the plugin's capability.
- Every backup's manifest has SHA-256 checksums of its files and is signed with a key only this site knows (HMAC-SHA256). Restores check both; a backup that can't be verified needs your explicit confirmation.
- Archives are only extracted inside the WordPress folder: absolute paths, `..` and symbolic links are refused, and wp-config.php is never overwritten.
- The database import only runs the statement types a dump needs.
- Passwords, keys and tokens are stored encrypted, never shown again in the admin, never returned by the REST API, and removed from logs and the system information report.
- SFTP servers are pinned by their host key fingerprint. OAuth sign-ins use your own app with PKCE.
- The restore tool, recovery link and installer use long random keys checked in constant time, lock out repeated wrong keys, and the restore tool and installer delete themselves when done. Restore links expire after an hour if not used.
- Migration links are signed, expire after 48 hours and only serve the files of that migration package.
- **Treat the recovery link, migration links and installer keys like passwords.**

---

## Deactivating and uninstalling

**Deactivating** the plugin:

- stops schedules, backups before updates and all background work (a running job continues when you activate the plugin again);
- leaves backups, settings, destinations and the recovery link in place. **The recovery link keeps working** while the plugin is inactive, because it doesn't need WordPress. That's on purpose: when a site is broken, plugins are often deactivated while you troubleshoot. Turn the link off on the Recovery tab first if you don't want that.

**Deleting** the plugin (Plugins → Delete):

- Always: removes the scheduled tasks, lock files, the recovery link (its tool in wp-content and its settings) and leftover restore tools. They open a restore without a login, and nothing would look after them once the plugin is gone. After a reinstall, create a new recovery link.
- With **Settings → Advanced → Also delete its data** on: also removes the settings, schedules, destinations, OAuth sign-ins, the list of backups at destinations, and the job files, logs and temporary files. On multisite, the main site's setting decides about the files.
- **Never:** backups are not deleted, not on this server (the `backups/` and `import/` folders stay) and not at your destinations. They may be the only copy of your site.
- With **Also delete its data** off (the default), everything else stays, so a reinstall picks up where you left off.

To remove everything:

1. Delete the backups you don't need on the Backups tab, choosing every place they're stored, and the packages under **Migration → Packages ready to import**.
2. Disconnect Google Drive, Dropbox and OneDrive destinations, and delete the apps you created with these providers if you don't need them.
3. Tick **Settings → Advanced → Also delete its data** and click **Save changes**.
4. Deactivate and delete the plugin. (The recovery link and restore tools are removed with it.)
5. Delete the `wp-content/uploads/symelo-backups-…` folder by FTP.

---

## For developers

### wp-config.php constants

| Constant | Values | Effect |
|---|---|---|
| `SYMELO_BACKUPS_KEY` | At least 32 random characters | Key that encrypts the stored secrets (destination passwords and keys, OAuth tokens, the encryption password, the manifest signing key) instead of a key derived from the site's security salts (`wp_salt( 'secure_auth' )`). Then a copy of the database alone can't reveal them. Shorter values are ignored. |
| `SYMELO_BACKUPS_SPEED` | `fast`, `normal` or `gentle` | Locks **Settings → Advanced → Speed**. Invalid values mean `normal`. |

Set `SYMELO_BACKUPS_KEY` when you install the plugin, and keep it when you move the site:

```php
define( 'SYMELO_BACKUPS_KEY', '…64 random characters…' );
```

Adding or changing it later makes everything encrypted with the old key unreadable: re-enter destination secrets, connect OAuth destinations again, set the encryption password again and create a new recovery link. A new signing key is created too, so existing backups show as from another site.

### Filters

| Filter | Default | Purpose |
|---|---|---|
| `symelo_backups_capability` | `'manage_options'` (`'manage_network_options'` on multisite) | Capability for the screen, every admin action and the REST API. |
| `symelo_backups_pace` | See [Advanced](#advanced) | `( array $profile, string $speed )`. Keys: `budget` (seconds per step), `pause` (seconds between steps), `max_load` (load per core at which work waits; 0 = never). |
| `symelo_backups_dispatch_args` | Non-blocking POST with the runner token | `wp_remote_post()` arguments of the request the site sends to itself. |
| `symelo_backups_http_timeout` | `60` | `( int $seconds, string $method, string $host )`. Timeout of requests to S3-compatible storage, B2 and Azure (minimum 5). |
| `symelo_backups_ftp_timeout` | `30` | FTP connection timeout in seconds (minimum 5). |
| `symelo_backups_sftp_timeout` | `30` | SFTP connection timeout in seconds (minimum 5). |
| `symelo_backups_azure_version` | `'2026-04-06'` | Azure Blob Storage REST API version. |
| `symelo_backups_pull_request_args` | – | `( array $args, string $url )`. Arguments of the requests that pull a package from another site. |
| `symelo_backups_pull_allow_private_urls` | `false` | `( bool $allow, string $url )`. Allow pulls from private and local addresses (for example two sites on one internal network). |
| `symelo_backups_notification` | – | `( array $mail )` with `to` (array), `subject`, `body`. Change or cancel (empty `to`) a notification email. |
| `symelo_backups_more_from_symelo` | `[]` | Products listed under "More from Symelo LTD" on the Support tab: `[ [ 'title' => …, 'description' => …, 'url' => … ] ]`. The card only appears when the list isn't empty. |

### Actions

| Action | Arguments | When |
|---|---|---|
| `symelo_backups_loaded` | `Plugin $plugin` | The plugin has loaded. |
| `symelo_backups_register_destinations` | `Registry $registry` | Register a custom destination with `$registry->add( new My_Destination() )`. Destinations implement `Symelo\Backups\Destinations\DestinationInterface`. |
| `symelo_backups_job_finished` | `array $job` | A job is done, failed or cancelled. Keys include `id`, `type`, `status`, `options`, `result`, `error`, `warnings`. |
| `symelo_backups_destination_saved` | `string $id, string $type, bool $new` | A destination was added or changed. |
| `symelo_backups_destination_deleted` | `string $id` | A destination was removed. |
| `symelo_backups_destination_tested` | `string $id, true\|WP_Error $result` | A destination was tested. |
| `symelo_backups_cleanup` | `JobManager $jobs` | After the daily cleanup. |

### WP-CLI

| Command | What it does |
|---|---|
| `wp symelo-backups backup [--components=<list>] [--destinations=<list>] [--type=<full\|incremental\|auto>] [--note=<note>] [--no-wait]` | Makes a backup and waits until it's finished, printing the progress. Components default to the settings, destinations to this server only, type to `full`. `--no-wait` only queues it. |
| `wp symelo-backups list [--format=<table\|csv\|json\|yaml\|ids>]` | Lists backups with ID, date, type, size, components, completeness, encryption, where they're stored and the note. |
| `wp symelo-backups restore <id> [--components=<list>] [--source=<id>] [--ask-password] [--confirm-foreign] [--no-replace-urls]` | Prepares a restore and prints the restore page's address. Nothing changes until you open it in a browser (within an hour) and start the restore there. |
| `wp symelo-backups run [--max-seconds=<seconds>]` | Starts the scheduled backups that are due, then works on queued jobs until none are left (or the time is up, minimum 10 seconds), without pauses. |
| `wp symelo-backups schedules [--format=<format>]` | Lists the schedules. |
| `wp symelo-backups destinations list` | Lists destinations with ID, name, type, whether they're on and available. |
| `wp symelo-backups destinations test <id>` | Tests a destination. |
| `wp symelo-backups self-test` | Checks archives, encryption, damage detection, a database dump and import of temporary tables, search-replace and S3 request signing on this server (10 to 30 seconds). |

Examples:

```sh
wp symelo-backups backup --components=database --note="Before the launch"
wp symelo-backups backup --destinations=s3-a1b2c3 --type=auto
wp symelo-backups restore 20261012-191500-full-a1b2c3 --components=plugins,themes
```

### REST API

All routes are under `/wp-json/symelo-backups/v1/` and need the plugin's capability: an administrator logged in (with a REST nonce) or using an application password. Responses never contain secrets: destination secrets only say whether they're set, and jobs leave out installer keys and migration links.

| Method and route | Parameters | What it does |
|---|---|---|
| `GET /status` | – | Version, free space, current job, number of active jobs, last successful backup, counts, recovery status, runner times. |
| `GET /backups` | – | All backups, newest first. |
| `POST /backups` | `components`, `destinations`, `type` (`full` default, `incremental`, `auto`), `note`, `encrypt`, `include_wp_config` | Starts a backup. Answers 202 with the job. |
| `GET /backups/{id}` | – | The manifest (without signature) plus `locations` and `foreign`. |
| `DELETE /backups/{id}` | `locations` (default: everywhere) | Deletes a backup in the background. |
| `GET /backups/{id}/files/{file}` | – | Streams a file of a backup on this server (`manifest.json` or a file the manifest lists). Supports HTTP Range. |
| `GET /jobs/current` | – | The running or next job. |
| `GET /jobs/{id}` | `log` (bool) | A job, optionally with the end of its log. |
| `POST /jobs/{id}/cancel` | – | Cancels a job. |
| `POST /run` | `seconds` (5–60, default 20) | Starts the scheduled backups that are due (`started`: schedule ID => job ID), then runs background work in this request, without pauses: an alternative to WP-CLI for external cron. |
| `POST /restore` | `backup_id` (required), `components`, `source`, `password`, `replace_urls` (bool or null), `delete_extra`, `confirm_foreign` | Prepares a restore (a job). When it's done, `result.restorer_url` (for the same user) is the restore page to open within an hour. |
| `GET /settings` | – | All settings, without secrets (`has_encryption_password` instead). |
| `POST /settings` | Any setting keys; `encryption_password` (write-only, `""` removes it) | Changes only the keys you send, validated like the settings screen. A new password needs at least 8 characters, and encryption can't be turned on without a password: both answer 400 and change nothing. Removing the password turns encryption off. Returns the settings, `errors` (values that were left out) and `messages`. |
| `GET /destinations` | – | Destinations with their values; secrets only as set or not, plus problems. |
| `POST /destinations` | `id` (leave out to add), `type`, `label`, `values` | Adds or changes a destination. Blank secrets keep the stored value. |
| `DELETE /destinations/{id}` | – | Removes a destination (backups stored there stay there). |
| `POST /destinations/{id}/test` | – | Tests a destination: `{ok, message}`. |
| `GET /schedules` | – | All schedules. |
| `POST /schedules` | `id` (leave out to add), `label`, `enabled`, `frequency`, `time`, `weekday`, `monthday`, `type`, `full_every`, `components`, `destinations`, `keep`, `keep_days` | Adds or changes a schedule. A new schedule takes what you leave out from the defaults: daily at 03:00, incremental with a full backup every 7 days, the components from the settings, this server, keep 7. |
| `DELETE /schedules/{id}` | – | Deletes a schedule (its backups stay). |
| `GET /recovery` | – | Recovery link status: `enabled`, `created`, `folder_ok`. |
| `POST /recovery` | – | Creates (or replaces) the recovery link. The response is the only place the link is shown. |
| `DELETE /recovery` | – | Turns the recovery link off. |
| `POST /migration/export` | `encrypt`, `note` | Starts a migration package. The response (202) is the only place with `result.installer_key`. |
| `POST /migration/{id}/link` | – | A new migration link for a package on this server: `{link, expires}`. |
| `GET /migration/{id}/installer` | – | Streams `installer.php` for a package. |
| `GET /migration/imports` | – | Packages in the import folder, with completeness and missing files. |
| `DELETE /migration/imports/{id}` | – | Deletes a package from the import folder. |
| `POST /migration/pull` | `link` | Pulls a package with a migration link (a job). |
| `POST /migration/import` | `backup_id`, `password`, `confirm_foreign` | Prepares an import (a job, like `/restore`). |
| `POST /self-test` | – | The self-test (as in WP-CLI). |
| `GET /oauth` | – | Public: the OAuth redirect target. It only forwards the provider's answer into wp-admin, where the administrator and the sign-in request are checked. |

The migration links' download endpoint is `wp-admin/admin-ajax.php?action=symelo_backups_pull`. It needs no login; every request carries a signature derived from the link, and only the files of that migration package are served until the link expires.

IDs: backups `{Ymd}-{His}-{type}-{random}` in UTC (for example `20261012-191500-full-a1b2c3`), jobs `job-{Ymd}-{His}-{microseconds}-{random}`, destinations `{type}-{random}` (for example `s3-a1b2c3`), schedules `{frequency}-{n}` (for example `daily-1`).

### File formats

A backup is a folder `backups/{id}/` on this server, and `{folder}/{site}/{id}/` at destinations (the site part is the site's host, port and path, for example `example.com`):

| File | Content |
|---|---|
| `manifest.json` | The description: site details, components, files with sizes and SHA-256 checksums, the chain, deleted paths, encryption mode and signature. Written (and uploaded) last. |
| `{id}.database.sql.gz` | The database dump (`.enc` when encrypted). |
| `{id}.files.{n}.tar.gz` | File archive parts, n = 1, 2, … (`.enc` when encrypted). |
| `{id}.index.gz` | The file index: one JSON line per file of the complete file set, with size, modification time and the backup that holds its current version (`.enc` when encrypted). |
| `installer.json` | Migration packages only: the bcrypt hash of the installer key. |

Details:

- Archives are POSIX ustar `.tar.gz` files. Paths are relative to the WordPress folder (`wp-content/uploads/2026/10/photo.jpg`, `wp-admin/index.php`). wp-config.php is stored as `symelo-meta/wp-config.php` in part 1. Each part is a standalone archive.
- The database dump is plain SQL, UTF-8, one statement per line: `DROP TABLE IF EXISTS`, `CREATE TABLE`, then `INSERT` statements, with the original table prefix. Binary values are written as hex. Views have no `DEFINER`. The last line is `-- symelo:complete`; a dump without it is incomplete.
- Files are written as a series of gzip members, which standard tools read as one `.gz` file.
- Encrypted files (`.enc`) start with the header `SYMBKENC` and can only be opened by the plugin, its restore tool or the installer.
- `docs/architecture.md` in the plugin folder describes every format in detail.

**Opening an unencrypted backup by hand:**

```sh
# Files: extract every part. For an incremental backup, extract the full
# backup first, then each incremental backup in order (see "chain" in
# manifest.json), and delete the paths listed under files.deleted.
mkdir site && cd site
tar -xzf ../20261012-191500-full-a1b2c3.files.1.tar.gz

# The old wp-config.php
cat symelo-meta/wp-config.php

# Database: import the dump of the newest backup in the chain
gunzip -c ../20261012-191500-full-a1b2c3.database.sql.gz > database.sql
mysql -u dbuser -p dbname < database.sql

# File index
gunzip -c ../20261012-191500-full-a1b2c3.index.gz | head
```

When the site moves to another address this way, replace the addresses afterwards, for example with `wp search-replace`.

### Scheduled tasks

| Hook | Schedule | Purpose |
|---|---|---|
| `symelo_backups_daily` | Daily | Applies all retention rules; deletes finished jobs and logs older than 30 days, temporary files older than a day, unfinished backup folders older than two days, abandoned package uploads older than a week, and unused restore tools; refreshes OAuth sign-ins unused for 7 days. |
| `symelo_backups_schedules` | Every 5 minutes | Starts the schedules that are due. |
| `symelo_backups_runner` | Every minute, only while jobs are queued or running | Fallback that keeps background jobs moving. |
| `symelo_backups_runner_next` | One-off | Continues a job after a pause (Gentle speed, a busy server, or a destination that asked to retry later). |

All of them are removed on deactivation.
