Build Your Own WordPress Plugin Update Server with a Serverless Function
A while ago, I wrote a premium WordPress plugin for creating typewriter effects with TypeIt via shortcode or Gutenblock. I was ill-prepared for the trickiness of managing plugin updates for such a plugin. Since a premium plugin is monetized, it can’t live inside the WordPress Plugin Repository like free options can, and asking paying customers to manually upload the a ZIP file for each new release is a clunky, less-than-ideal option.
The go-to solution for this problem is to set up a custom update server. With this approach, your plugin checks for updates the same way any other plugin does, but instead of asking the Plugin Repository for those updates, it’s configured to ask your server.
Just a short while ago, setting this up was quite a chore, since it required you to create and maintain a separate application just to allow your users to easily get updates. But with the serverless/lambda function landscape now so accessible, it’s pretty simple to roll one of these on your own with neither much hassle nor cost.
Let’s explore just how simple that is by setting up own serverless plugin update function that can be used to manage an unlimited number of your premium plugin updates, as well as what it takes to configure a plugin to actually use this function.
The number of companies offering serverless function platforms with minimal configuration are plenty, with the top two contenders arguably being Netlify and Vercel. For this setup, we’ll be using the latter, although the fundamentals are largely similar, with the primary differences being the proprietary tools and configuration patterns required by each.
On your machine, create a directory and initialize it as a Git repository.
mkdir wp-plugin-update-function-example
cd wp-plugin-update-function-example
git init
After installing and authenticating with the Vercel CLI, initialize your project by running vercel and following the prompts. See Vercel’s solid documentation on then linking this new project with a remote Git repository. Once you’re finished, a .vercel directory containing some project configuration should have been generated in the project on your machine.
After the project is initialized, spin up a local development server by running vercel dev. You should see a message indicating that the server is available at http://localhost:3000 (assuming nothing else is running there already).
Create an api directory and a file named [plugin].js. This’ll serve as a path segment that we’ll use to pass plugin slugs through to the function. You could also go with the URL query parameter approach. That decision is up to you.
mkdir api &&touch"api/[plugin].js"
Inside that newly created [plugin].js file, let’s start with something simple just to verify things are working. Using this, we’ll pull the plugin parameter from the URL path – dictated by the name of our file – and immediately spit back a bit of JSON.
Now, navigate to http://localhost:3000/api/our-plugin, and you should see some JSON output on the screen. The structure of that blob is important; it’s what we’ll later use to communicate to a WordPress site wondering if a plugin should be updated. More on that as we go.
Next, let’s make a means of surfacing information about a particular plugin whenever a GET request is made to our endpoint.
First off, we’ll create a directory to store the ZIP files for our different plugin versions. Go ahead and create a plugins/our-plugin directory, and place a zipped version of our plugin within that.
We’re building this function in a way that only requires to you to upload newer ZIP files to the function’s repo as new plugin releases come out, leaving the older versions in place. It’s simpler to use, and it’s also handy in case customers request a download link for an older version of a plugin. If you don’t need this level of complexity, feel free to rip stuff out as desired.
The naming convention for that file is important, since we’ll use that for determining what the latest available version is. It should named by the version represented by the zipped contents. For example, 1.0.2.zip. With that ZIP added to the mix, our directory tree should look something like this:
Now, if you make a request to our function using a non-existent plugin slug, you should get a 404 status code, which is a bit more appropriate for the type of error that occurred.
And just like that, we have our own custom plugin update server ready in place. The next piece is configuring our plugin to check for updates & pull from that endpoint, rather than the WordPress Plugin Repository.
WordPress uses an update_plugins transient to keep track of which plugins have been checked for updates recently, and which are due for another check. We can hook into & filter the value of that transient using the site_transient_update_plugins filter. By doing this, we’ll be able seize control of where WordPress looks to see if an update is ready.
But before we start to stub out that filter, let’s write a simple function for fetching fresh plugin data via GET request to our endpoint. With the following, we’re making that request, accounting for a possible error, and returning the raw JSON.
define("PLUGIN_CHECK_ENDPOINT","http://localhost:3000/api/our-plugin");functionfetch_remote_data(){$remoteData=wp_remote_get(PLUGIN_CHECK_ENDPOINT,['headers'=>['Accept'=>'application/json']]);// Something went wrong!if(is_wp_error($remoteData)||wp_remote_retrieve_response_code($remoteData)!==200){returnnull;}// {// "version":"1.0.2",// "package":"http://localhost:3000/plugins/our-plugin/1.0.2.zip"// }returnjson_decode($remoteData['body']);}
Now, let’s tweak that to return an object with an interface expected by WordPress’s plugin update system, populating the new_version and package properties with the data from our endpoint. (If you wanna dig into that more, see here.) As for the rest, feel free to fill things in as needed.
Now, we’re ready to start filtering the update_plugins transient. To start, let’s stub out our filter and fetch the fresh data using the function we just wrote above. We’ll also pull in a means of grabbing the current installed version of our plugin, since we’ll later need to that to compare against our remote version.
The magic here is in that $transient->PROPERTY stuff. If our remote version is larger than the version we have installed, we’ll tell WordPress to prompt for an update by sticking it on the response property, which is an array whose keys are a WordPress site’s plugins’ root files. But if the plugin is not eligible for an update, we’re sticking it onto the no_update property, and no update will be prompted.
With things as they are, we’re technically sorta done. If you artificially set your local plugin’s version to something lower than what the remote endpoint is providing, you’ll see an update prompt:
But! Unfortunately, since our update endpoint is on localhost, WordPress will, by default, not allow a ZIP file to be pulled from it. So, for local testing, let’s turn that feature off using the filter below. Ideally, don’t put this in your plugin’s code. You don’t want to accidentally ship it.
After that’s in place, there’s another key step: commit & push up your plugin’s code, since it’s about to get wiped out with a “new” version, and you’ll need a way to revert it. Then, go ahead and attempt to run the update from the WordPress admin. It should work.
But! There’s still work to be done. In its current state, our code requires that our endpoint be hit every time it’s run. That’s an unnecessarily burdensome process (you’ll likely notice it every time you refresh the WordPress admin), especially when we have transients at our disposal.
Using a simple transient, we can cache the payload from our endpoint until a certain amount of time passes. Let’s set one to expire after 12 hours, and set up some simple logic to first check for a transient value before hitting our serverless function.
Now, if you were to refresh your WP admin a few times, you’ll notice that things are performing much more smoothly, since our serverless function will only need to be hit every 12 hours at the most.
Everything you’re reading here was put into practice with one of my own plugins. If you’d like to take a glace at that code (with some various adjustments to suite my specific needs), check out the repositories below:
One of the primary motivators I had for writing this was the fact that I couldn’t find any ultra-clear documentation for the process myself. That said, I welcome any feedback or unexpected gotchas you hit along the way.
Thanks for reading!
Alex MacArthur is a software engineer working for Dave Ramsey in
Nashville-ish, TN. Soli Deo gloria.
Get irregular emails about new posts or projects.
No spam. Unsubscribe whenever.
Leave a Free Comment
3
comments
John Jago
Thank you for creating this guide. It was extremely helpful as someone who is creating their first premium plugin and wants a minimal infrastructure for providing updates. In case anyone is wondering, I did not encounter the transient issue or the missing enable autoupdate issue described in the other comments, so it must be something specific to one’s setup.
Adam Kent
Hi Alex, thanks for the guide. I've added my plugin to the $transient->response and returned it, and I can see by dumping out the $transient after that my plugin is included in the response (with the same schema as the other plugins), but for some reason the update notice isn't added to my plugin and it doesn't have a the badge in the menu either. It also doesn't appear under updates available list. Any ideas?
Lutz
Hi Alex, do you know how to make a custom plugin to show the "enable autoupdate" button? This button seems just to be visible while it's outdated and the "update now" button is visible.