General, automated WordPress-to-WordPress sync is unsolvable

Adam Zieliński Avatar

Posted by

on

Twenty-three years into WordPress, the general problem of site synchronization is still unsolved. We have great tools for the slices, but nothing for the whole. You can’t reliably automate any of these tasks beyond basic sites:

This post exhaustively covers what makes these problems so difficult, and what we can do instead. It is a 2600 words deep dive. It reflects many discussions I’ve had with Dennis Snell over the years, as well as what I’ve learned from WordPress.com, the WordPress importer plugin, the Site Transfer protocol, Playground, the more recent Reprint, ForkPress, and more.

Get some coffee, clear your calendar for an hour, and join me on this ride!

What’s difficult about syncing?

The holy grail of WordPress↔WordPress synchronization is this:

  1. I connect Site A to Site B.
  2. I can now click Push or Pull buttons to move the data in either direction.
  3. Optionally, I can choose which posts, products, or plugins I want to sync.

It’s effectively the same as a hostless, Peer↔Peer WordPress.

I claim that such an automated, generalized sync is not possible.

There are more limited forms of syncing WordPress could have. We’ll explore them later in this post, but now let’s discuss the constraints we’re facing.

Migrating a site to a different environment

A WordPress Site = Database + Files + Environment. We can only sync Site A with Site B if their environments match closely enough. That is, all of these and more:

When the environments don’t match, we’ll observe:

…and many, many more issues. Some are benign, and some are catastrophic. Some are solvable, and some aren’t. The absolute worst scenario is when the site looks good after syncing, and then two weeks later, we detect a serious and irreversible data leak, loss, or corruption.

We can’t automatically detect most of these mismatches. How would we know Site B only runs 2 PHP workers? Or that our 24th plugin relies on Redis in specific code paths? Or that Site A had a special thumbnail-generating route? The PHP extensions rarely match exactly, but how do we know if that matters? The best we can do is compare the PHP versions, do a few more naive checks like that, and hope for the best.

Synchronizing too much is catastrophic

Let’s categorize every database field as environment-specific or not environment-specific. Woo orders, MySQL credentials, Stripe keys, salts, connected OAuth clients, and site URLs are all examples of that, as they always differ between production sites and testing sites.

If you sync local environment-specific data to production, your service will be disrupted. It could be a full site outage, checkout outage, lost or mismatched orders, misattributed emails, or all these things. Or something else entirely. The point is: corrupt this mission-critical data and there will be trouble.

Therefore, we must not synchronize any environment-specific data.

However, no sync mechanism can tell which data is environment-specific.

Codex analyzed a few popular WordPress plugins for me and found the following environment-specific records:

Who knows where else they store such data? In site options? In post meta? In block attributes? And who knows which rows are required for the site to function? And how could we replace them during the sync?

We don’t have a reliable, exhaustive, and continuously updated list of all the plugins, their versions, and the locations of the environment-specific information they store. And what about dedicated plugins? I know a site using ~50 plugins developed in-house. How do we account for that?

Pulling environment-specific data to the local development site is also risky. Your test order could send an email to a customer, update the real accounting records, and dispatch shipping via a webhook. Not to mention the legal and security risks of a contractor pulling the PII of your customers and your production Stripe key.

Here’s another take: Let’s categorize every database field as sensitive or not sensitive. PII, API credentials, and credit cards are all sensitive. Pulling sensitive data into the local development environment creates a security and legal risk, but there’s no automated way to know which piece of information is sensitive.

Synchronizing too little may still break the site

On the flip side, we can’t just synchronize the posts. We don’t know what a post is.

For sure, a post is an entry in wp_posts with a few related entries in wp_post_meta. It could also consist of a few related media uploads, also stored in wp_posts and the wp-content/uploads/ directory.

But is the post also:

Any sync mechanism would need to decide. Bindings from plugins would help. Using UUID as post_id or guid would also help. And yet, missing one of these could break the site, so in the end, the user would still have to make choices.

Identifying semantic conflicts requires domain knowledge

A conflict is when two identical sites changed in different ways with different guiding intent. The code doesn’t know whether that happened.

When we talk about conflicts, most of the time we mean row-level conflicts. Site A updated the post_content, Site B updated it to a different value, and we need to merge both edits.

Semantic conflicts are trickier. There’s no clear row-level disagreement, and yet both sites clearly disagree. Here are a few examples of that:

At the row level, there are no conflicts. However, if we just apply local changes to the production site, the results won’t be what we wanted.

Git also has this problem: your PR changes the grid layout to row layout, my PR adds a hamburger menu to every row. No lines of code conflicted, but the effect is still not what either of us wanted.

However, Git operates on text files, where this type of problem is much easier to catch than in the WordPress database. WordPress site sync is as if Git could auto-merge .sqlite files, and each of us changed 100 interrelated rows. Who could really review that?

Conflict resolution requires plugin support and human intervention

Plugins store their own database records. We can’t synchronize two custom tables without domain knowledge. The plugin must provide the bindings for conflict resolution in its own data:

Note how complex and comprehensive these plugin bindings would have to be! How many plugin shops would build and maintain them?

The more difficult scenario is when even the best bindings couldn’t possibly reconcile the data.

Because the code can’t capture the user’s intent, these conflicts could only be resolved manually:

For these, we’d need a case-by-case UI, and the plugins would have to support it.

Here’s a different conundrum. Just because we can merge the data automatically, should we? Real-time collaboration software often uses CRDTs. It’s really great at merging, and it still gets things wrong.

Solutions that won’t work for general site sync

It’s so very tempting to say this or that simplified solution would address 90% of sites. Here are a few such solutions that, sadly, wouldn’t work:

State of the art: What is the community doing?

Here’s the rundown of a few projects that approached these problems over the years:

What can be done instead?

Instead, we can solve smaller, well-scoped problems—and solve them well enough:

Reprint supports a few of those already and can be bent to support the rest. It may never handle every WordPress site, but it doesn’t need to be universal to matter. It just has to help you move your data, safely, wherever you’re going.

Many thanks to Dennis Snell for reviewing this post and helping me refine it. And also for the many conversations we’ve had where these insights were developed.

Leave a Reply

Discover more from Adam's Perspective

Subscribe now to keep reading and get access to the full archive.

Continue reading