Skip to content
  • FG-60
  • 13 min read
  • Last checked November 7, 2026

How to Push WordPress Staging to Live Without Losing Orders, Form Entries or Comments

A full staging push overwrites every order, customer, review and form entry that arrived on live since you cloned it. Here is how to deploy changes and keep that data.

Run the health check

You spent two weeks on a staging copy: a new design, a reworked checkout, a few new plugins. You press Push to live, and the site looks perfect. Then the emails start. Customers cannot log in, yesterday’s orders have vanished from WooCommerce, a payment notification points to an order that is now someone else’s, contact form entries are gone and the latest reviews have disappeared.

Nothing is broken in the usual sense. The push did exactly what it was told: it copied the staging database over the live one. Staging was a snapshot of live from the day you cloned it, so everything that happened on live since then was replaced by the older copy. This guide is for store owners, designers and freelancers who need to deploy staging work without that loss, and for anyone who has just pushed and needs to understand what went missing.

What is happening

The moment you clone a site, you have two databases that start identical and then drift apart. Live collects customer activity. Staging collects your design and configuration work. A database push cannot merge them; it picks one side and throws the other away.

Changes on live (customer activity) Tables
Orders (High-Performance Order Storage) wp_wc_orders, wp_wc_orders_meta, wp_wc_order_addresses, wp_wc_order_operational_data
Orders (legacy storage, or HPOS with sync on) wp_posts (type shop_order), wp_postmeta
Order line items wp_woocommerce_order_items, wp_woocommerce_order_itemmeta
Store analytics wp_wc_order_stats, wp_wc_order_product_lookup, wp_wc_customer_lookup
Customers and passwords wp_users, wp_usermeta
Comments and product reviews wp_comments, wp_commentmeta
Stock levels wp_postmeta (_stock and _stock_status on products)
Form entries Gravity Forms wp_gf_entry*, WPForms wp_wpforms_entries, Formidable wp_frm_items, Fluent Forms wp_fluentform_submissions; Flamingo stores Contact Form 7 messages as posts
Scheduled jobs and renewals wp_actionscheduler_*, the cron row in wp_options

Your staging work, meanwhile, usually lives in wp_posts and wp_postmeta (pages, templates, menus, builder layouts), wp_options (theme, builder and plugin settings), the term tables (categories, menu assignments) and any tables new plugins created.

Why a blanket database push is dangerous

Look at the overlap: wp_posts, wp_postmeta and wp_options appear on both sides. You cannot push your new pages without also pushing the old stock levels, and on legacy stores, the old order list. Worse, both sites have been handing out new IDs from the same starting point. Staging gave ID number N to your new landing page; live gave the same ID N to a customer’s order. Push staging and that order becomes a landing page.

A full push also carries staging-only settings to live: Discourage search engines from indexing this site (the blog_public option), payment gateways in test or sandbox mode, disabled email delivery, a different site URL baked into options, and license keys tied to the staging domain. Each of these breaks live in a quieter way than missing orders.

Before you start

  • Access you need: wp-admin on both sites, SSH and WP-CLI on both (most of the steps below), and your host’s staging tool if you use one.
  • Backups: take a full live backup immediately before any deployment, and keep the last backup taken before any push you have already done. If something goes wrong, that file is the only copy of the missing data.
  • How to run commands. Commands starting with wp or rsync run in an SSH terminal on the server, from the site folder containing wp-config.php. Each step says whether it runs on staging or live. Some hosts call WP-CLI wp-cli. SQL shown with wp db query can also be run in phpMyAdmin.

Step 1: Measure what changed on each side

You need two lists: what live gained since the clone, and what you changed on staging. Start with live. Over SSH on the live site:

wp db query "SELECT COUNT(*) AS orders FROM wp_wc_orders WHERE date_created_gmt > 'CLONE_DATE'"
wp db query "SELECT COUNT(*) AS users FROM wp_users WHERE user_registered > 'CLONE_DATE'"
wp db query "SELECT COUNT(*) AS comments FROM wp_comments WHERE comment_date_gmt > 'CLONE_DATE'"

CLONE_DATE is when staging was created, in UTC, written as YYYY-MM-DD HH:MM:SS. Your host’s staging screen usually shows it; otherwise use the newest user_registered date on staging as a close guess. wp_ is the default table prefix: check yours with wp config get table_prefix. On a legacy store with no wp_wc_orders table, count orders with SELECT COUNT(*) FROM wp_posts WHERE post_type='shop_order' AND post_date_gmt > 'CLONE_DATE'. Do the same for your form plugin’s entries table. Any non-zero number is data a full push would erase.

Now staging. Over SSH on the staging site:

wp db query "SELECT ID, post_type, post_status, post_title FROM wp_posts WHERE post_modified_gmt > 'CLONE_DATE' AND post_type NOT IN ('revision','shop_order') ORDER BY post_type"

This lists every page, template, menu item, product and builder item created or edited on staging since the clone, with its ID. Keep the output: it is your deployment list. Compare plugin lists on both sites with wp plugin list --fields=name,status,version to see which plugins you added or updated, and compare tables with wp db tables --all-tables-with-prefix to spot tables only staging has.

Step 2: Choose the right deployment strategy

What you changed on staging Best strategy
Theme or child theme code, custom plugin code, CSS Files-only push (Step 3)
New or updated plugins Install and update them on live directly, then copy their settings (Step 5)
A handful of pages, templates or builder layouts Export and import those items, or rebuild them on live (Step 4)
Settings in the Customizer, theme options or plugin screens Copy specific options (Step 5) or re-enter them on live
Almost everything, on a site with no orders, users or forms Full push with a freeze window (Step 6)
Almost everything, on an active store Freeze window plus selective push, or a merge tool, never a blind full push

Merge and deployment tools exist for this problem. Some host staging tools let you push files only or pick tables; some plugins push selected tables or post types; a few track changes on staging and replay them on live. Before trusting any of them, find out exactly which tables a push touches, and run it once on a copy of live.

Step 3: Push files only

If your changes are code, the database never needs to move. Many host staging tools have a files-only push option. By hand, over SSH on the live server, you can pull the theme from staging:

rsync -avz --dry-run --delete STAGING_USER@STAGING_HOST:/STAGING/PATH/wp-content/themes/THEME_FOLDER/ wp-content/themes/THEME_FOLDER/

STAGING_USER and STAGING_HOST are your SSH login and server for staging (use a local path if both sites share a server). /STAGING/PATH is the staging site folder, and THEME_FOLDER is the theme’s folder name. -a keeps permissions and timestamps, -v lists each file, and -z compresses in transit. --delete removes files on live that no longer exist on staging, which keeps the folders identical. --dry-run shows what would change without copying: read the list, and when it shows only the files you expect, run it again without --dry-run. Do not rsync the whole wp-content folder, because uploads on live holds files customers and editors added since the clone.

Step 4: Move specific pages and layouts

Do not copy rows from staging’s wp_posts into live’s by ID; Step 1 showed why the IDs collide. Use tools that create new IDs on live instead:

  • WordPress export and import. On staging, Tools, Export can export pages or a custom post type as an XML file. On live, Tools, Import, WordPress imports them as new items. Menus and featured images need checking afterward.
  • Your page builder’s template export. Most builders can export a page or template as a file and import it on another site. That is usually the cleanest route for designed pages.
  • Rebuild on live. For two or three pages, copying and pasting the content or layout on live is often fastest and has zero risk.

If you replace an existing live page, import the new version as a draft, compare, then swap the content and keep the original page ID and URL, so links and SEO are not affected.

Step 5: Copy plugin and theme settings one option at a time

Never import the whole wp_options table: it contains live’s payment settings, license keys, cron schedule, WooCommerce settings and site URL. Copy only the rows you changed. Over SSH on staging, find the option name:

wp option list --search="*PLUGIN_KEYWORD*" --fields=option_name,autoload
wp option get OPTION_NAME --format=json > ../OPTION_NAME.json

PLUGIN_KEYWORD is part of the plugin or theme name, such as its slug; the asterisks are wildcards. OPTION_NAME is a name from that list, such as theme_mods_THEME_FOLDER for Customizer settings. --format=json writes arrays and nested settings as JSON, which keeps their structure intact. Then copy the file to live and, over SSH on live:

wp option get OPTION_NAME --format=json > ../OPTION_NAME.live-backup.json
wp option update OPTION_NAME --format=json < ../OPTION_NAME.json

The first line saves live’s current value so you can put it back. The second reads the staging value from the file. Good output is Success: Updated 'OPTION_NAME' option. or Success: Value passed for 'OPTION_NAME' option is unchanged. Before updating, open the JSON file and search it for the staging domain; if it appears, replace it with the live domain first, or run wp search-replace for that option afterward as shown in Step 7.

Step 6: Use a freeze window for a full or large push

When the changes are too broad to move piece by piece, a freeze window turns an unsafe push into a safe one by stopping live from changing while you work.

  1. Pick a quiet time and tell editors and staff. Note the start in UTC.
  2. Stop new data on live. Turn on a maintenance page, or at least disable checkout, account registration, comments and forms.
  3. Take a final live backup now, after the freeze has started.
  4. Copy live’s customer data into staging: export the live-owned tables from the table above and import them into staging, so staging becomes the complete, current version.
  5. Push staging to live, then run the checks in Step 8.
  6. Lift the freeze.

The fourth item, copying live’s data, looks like this over SSH on live:

wp db export ../live-owned.sql --tables=wp_wc_orders,wp_wc_orders_meta,wp_wc_order_addresses,wp_wc_order_operational_data,wp_woocommerce_order_items,wp_woocommerce_order_itemmeta,wp_users,wp_usermeta,wp_comments,wp_commentmeta,FORM_TABLES --single-transaction

FORM_TABLES is a comma-separated list of your form plugin’s tables, from wp db tables --all-tables-with-prefix. Adjust the prefix if yours is not wp_. --tables limits the export to those tables, and --single-transaction takes a consistent snapshot. Import the file on staging with wp db import ../live-owned.sql, then run the URL replacement from Step 7 on staging. On a legacy store, orders, stock and Flamingo messages are in wp_posts and wp_postmeta alongside your page edits; this whole-table approach cannot separate them, so use Steps 3 to 5 instead or get a developer to merge rows by type.

Step 7: Run search-replace in the right direction

Data moving toward live must end up with live URLs. Data moving toward staging must end up with staging URLs. Run the replacement on the destination copy after the import, or on an export file, never on live in the staging direction. To produce a live-ready SQL file from staging without changing the staging database, over SSH on staging:

wp search-replace 'https://staging.example.com' 'https://example.com' TABLE_ONE TABLE_TWO --skip-columns=guid --precise --export=../to-live.sql

staging.example.com is the staging address and example.com the live one, exactly as wp option get home shows them on each site. TABLE_ONE TABLE_TWO are the tables you are deploying (leave them out only for a full push). --skip-columns=guid leaves post GUIDs untouched, --precise handles serialized data in PHP for every column, and --export writes the transformed tables to a file instead of saving to the database. Run it once with --dry-run in place of --export to see the counts first. A count of zero means a typo or an http versus https mismatch.

Step 8: Check live straight after the deployment

  • Compare order, user, comment and form entry counts with the numbers from Step 1. They must be equal or higher, never lower.
  • Check wp option get blog_public on live: 1 means search engines are allowed, 0 means the staging setting came across.
  • Open each payment gateway’s settings and confirm it is in live mode, not test or sandbox mode.
  • Run wp rewrite flush, purge every cache and the CDN, and place a real low-value test order.
  • Check WooCommerce, Status, Scheduled Actions for failed or duplicated jobs.

Step 9: If you already pushed and data is missing

Put the store into maintenance mode first, so new orders do not reuse more IDs. Then restore the backup taken just before the push into a separate database, not over live, and list what exists there but not on live, such as orders created after CLONE_DATE. Small numbers of orders and users can be re-created by hand, matching payment transaction IDs in your gateway dashboard. Larger gaps need a careful row-by-row merge. The restore guide (FG-57) covers keeping and reconciling recent orders in more detail.

Advanced: stop ID collisions before they happen

The collision problem comes from both databases continuing the same auto increment sequence. When you create a staging site, push staging’s counters far ahead of live’s so that anything created on staging gets IDs live will not reach for a long time. Over SSH on staging, right after cloning:

wp db query "ALTER TABLE wp_posts AUTO_INCREMENT = HIGH_START_ID"
wp db query "ALTER TABLE wp_postmeta AUTO_INCREMENT = HIGH_START_ID"
wp db query "ALTER TABLE wp_terms AUTO_INCREMENT = HIGH_START_ID"

HIGH_START_ID is a number well above live’s current highest ID: check live with wp db query "SELECT MAX(ID) FROM wp_posts" and pick something many times larger than the number of items live will create during the project. ALTER TABLE ... AUTO_INCREMENT sets the next ID the table will hand out. Repeat it for wp_term_taxonomy and any builder tables you plan to deploy. Good output from wp db query "SHOW TABLE STATUS LIKE 'wp_posts'" shows the new value in the Auto_increment column.

With non-colliding IDs, selective row copies become possible: you can export only staging’s new rows with wp db export --tables=wp_posts --where="ID >= HIGH_START_ID" --no-create-info and the matching wp_postmeta rows by post_id, and import them into live without overwriting anything. --no-create-info leaves out the DROP TABLE and CREATE TABLE statements, so the import only adds rows. An import that hits an existing ID fails with a duplicate key error instead of overwriting, which is exactly the safety you want. Edits to pages that existed before the clone still need Step 4.

When to contact your host

Open a ticket before using a host staging tool if its documentation does not say which tables a push overwrites, or straight away if a push has already removed live data. Send the live and staging URLs, the time of the push in UTC, the clone date, the counts from Step 1, whether the store uses HPOS or legacy order storage, and which backups you need restored and where (a separate database, not over live). Ask whether the host keeps an automatic backup from just before the push. For checkout problems that remain after the data is back, start from the Emergency Room.

Still stuck?

Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.

Get the free checklists

Keep reading

All field guides

Search the field manual

What is going wrong?

Press / to search from any page, Esc to close