How to Configure Self-Hosted Updates for Your Private Plugins

In this tutorial I will show you how to develop a custom update repository for your private plugin. It will be helpful if you’re developing plugins for sale.

We are not going to use any ready or bloated PHP-libraries for that, only two filter hooks plugins_api and site_transient_update_plugins and transient cache, that’s all.

You can also download the full code as a ready plugin from my GitHub.

WordPress plugin update from a custom server

And remember if you need some custom coding help, you can always get in touch with me and my team.

And now let’s go do it!

1. Prepare your Custom Update Server

Update server could be anything, it can be a subdomain or just a directory on your website.

The long story short, here is the screenshot of the files on my update server.

Custom update server files
misha-update-checker.zip is the new version of the plugin, info.json contains all the update information, more on that below and two images that will be displayed on WordPress admin when viewing plugin information.

Let’s configure our info.json file now.

{
	"name" : "Misha Update Checker",
	"slug" : "misha-update-checker",
	"author" : "<a href='https://rudrastyh.com'>Misha Rudrastyh</a>",
	"author_profile" : "http://profiles.wordpress.org/rudrastyh",
	"version" : "2.0",
	"download_url" : "https://rudrastyh.com/wp-content/uploads/updater/misha-update-checker.zip",
	"requires" : "3.0",
	"tested" : "5.8",
	"requires_php" : "5.3",
	"last_updated" : "2021-01-30 02:10:00",
	"sections" : {
		"description" : "This simple plugin does nothing, only gets updates from a custom server",
		"installation" : "Click the activate button and that's it.",
		"changelog" : "<h4>1.0 –  1 august 2021</h4><ul><li>Bug fixes.</li><li>Initital release.</li></ul>"
	},
	"banners" : {
		"low" : "https://rudrastyh.com/wp-content/uploads/updater/banner-772x250.jpg",
		"high" : "https://rudrastyh.com/wp-content/uploads/updater/banner-1544x500.jpg"
	}
}

Also do not forget to check if info.json is valid, you can do it with any json validator you find in google, I used this one for example.

Another way is to generate info.json via PHP, here is how you can do it.

$update = array(
	'name' => 'Misha Update Checker',
	'slug' => 'misha-update-checker',
	
	...
	
);

header( 'Content-Type: application/json' );
echo json_encode( $update );

The advantage of PHP method is that you can pass some get parameters to it like info.php?plugin_id=5 or check plugin license as well.

Please note, that this json-file could have any parameter names, it doesn’t matter in this step. It matters below, when we pass these parameters into WordPress Plugins API.

Also an important note about the files and folders structure inside a zip archive.

No 👿 Yes ✔️
misha-update-checker.zip
––misha-update-checker.php
––readme.txt
misha-update-checker.zip
––misha-update-checker/
––––misha-update-checker.php
––––readme.txt

Here is what happens if you skip this advise: after you click the “Update” button, your old version of the plugin will be removed, but the new one won’t replace it but will be created in a randomly generated folder like misha-update-checker-xhfuif/ as a result you will get a notice: “The plugin has been deactivated due to an error: Plugin file does not exist.”

2. Plugin Information Modal

Usually when you click “View Details”, WordPress shows you the appropriate plugin information from its own wordpress.org update server. And the most interesting thing is that this info can be hooked before WordPress connects to wordpress.org server. 

Here is what I am talking about:

custom pugin modal window with update information

For the simplicity of the code I skipped using Transients API to cache the remote request, but you have to use it in your code. Check the plugin code on GitHub.

add_filter( 'plugins_api', 'misha_plugin_info', 20, 3);
/*
 * $res empty at this step
 * $action 'plugin_information'
 * $args stdClass Object ( [slug] => woocommerce [is_ssl] => [fields] => Array ( [banners] => 1 [reviews] => 1 [downloaded] => [active_installs] => 1 ) [per_page] => 24 [locale] => en_US )
 */
function misha_plugin_info( $res, $action, $args ){

	// do nothing if this is not about getting plugin information
	if( 'plugin_information' !== $action ) {
		return false;
	}

	// do nothing if it is not our plugin
	if( plugin_basename( __DIR__ ) !== $args->slug ) {
		return false;
	}

	// info.json is the file with the actual plugin information on your server
	$remote = wp_remote_get( 
		'https://rudrastyh.com/wp-content/uploads/updater/info.json', 
		array(
			'timeout' => 10,
			'headers' => array(
				'Accept' => 'application/json'
			) 
		)
	);

	// do nothing if we don't get the correct response from the server
	if( 
		is_wp_error( $remote )
		|| 200 !== wp_remote_retrieve_response_code( $remote )
		|| empty( wp_remote_retrieve_body( $remote ) 
	) {
		return false;	
	}

	$remote = json_decode( wp_remote_retrieve_body( $remote ) );
	
	$res = new stdClass();
	$res->name = $remote->name;
	$res->slug = $remote->slug;
	$res->author = $remote->author;
	$res->author_profile = $remote->author_profile;
	$res->version = $remote->version;
	$res->tested = $remote->tested;
	$res->requires = $remote->requires;
	$res->requires_php = $remote->requires_php;
	$res->download_link = $remote->download_url;
	$res->trunk = $remote->download_url;
	$res->last_updated = $remote->last_updated;
	$res->sections = array(
		'description' => $remote->sections->description,
		'installation' => $remote->sections->installation,
		'changelog' => $remote->sections->changelog
		// you can add your custom sections (tabs) here
	);
	// in case you want the screenshots tab, use the following HTML format for its content:
	// <ol><li><a href="IMG_URL" target="_blank"><img src="IMG_URL" alt="CAPTION" /></a><p>CAPTION</p></li></ol>
	if( ! empty( $remote->sections->screenshots ) ) {
		$res->sections[ 'screenshots' ] = $remote->sections->screenshots;
	}

	$res->banners = array(
		'low' => $remote->banners->low,
		'high' => $remote->banners->high
	);
	
	return $res;

}

On line 15 we get the plugin slug using plugin_basename( __DIR__ ), I would like to talk more on that:

plugin_basename( __DIR__ )misha-update-checker
plugin_basename( __FILE__ )misha-update-checker/misha-update-checker.php

And please keep in mind, that the link “View details” won’t appear until you implement the next step of this tutorial.

stdClass object properties

In the above code we created stdClass object with some parameter to pass into WordPress API.

name
Plugin name.
slug
Plugin slug.
author
Plugin author.
author_profile.
Author profile URL on wordpress.org, example https://profiles.wordpress.org/rudrastyh/.
contributors
The associative array of contributors wordpress.org profile name and URLs, like
array(
	'rudrastyh' => 'https://profiles.wordpress.org/rudrastyh',
	'contributor' => 'https://profiles.wordpress.org/contributor',
)
version
Current plugin version available to install.
tested
The latest WordPress version plugin tested with.
requires
The minimum WordPress version required.
requires_php
The minimum PHP version required.
rating
The rating count from 1 to 100
ratings
The 5-star rating, how much votes for each star, example:
array(
	5 => 2104,
	4 => 116,
	3 => 64,
	2 => 57,
	1 => 175,
)
num_ratings
The amount of overall votes
support_threads
The number of threads on support forum
support_threads_resolved
The number of resolved threads on support forum
active_installs
The number of active plugin installations
added
The date in YYYY-MM-DD format
last updated
The date in YYYY-MM-DD format
homepage
The plugin homepage URL
reviews
The HTML of plugin reviews.
versions
The array of plugin versions and their URLs, example
array(
	'1.0' => 'URL of zip archive of 1.0 plugin version',
	'trunk' => 'URL of the latest plugin version',
)
donate_link
The URL of donate link

Push the Update Information into WP Transients

This is the final step, after implementing it you will got this:

WordPress plugin update from a custom server

And again, for the simplicity of the code below I skipped using Transients API to cache the remote request, but you have to use it. Check how in the plugin code on GitHub.

add_filter( 'site_transient_update_plugins', 'misha_push_update' );
 
function misha_push_update( $transient ){
 
	if ( empty( $transient->checked ) ) {
		return $transient;
	}

	$remote = wp_remote_get( 
		'https://rudrastyh.com/wp-content/uploads/updater/info.json',
		array(
			'timeout' => 10,
			'headers' => array(
				'Accept' => 'application/json'
			)
		)
	);

	if( 
		is_wp_error( $remote )
		|| 200 !== wp_remote_retrieve_response_code( $remote )
		|| empty( wp_remote_retrieve_body( $remote ) 
	) {
		return $transient;	
	}
	
	$remote = json_decode( wp_remote_retrieve_body( $remote ) );
 
		// your installed plugin version should be on the line below! You can obtain it dynamically of course 
	if(
		$remote
		&& version_compare( $this->version, $remote->version, '<' )
		&& version_compare( $remote->requires, get_bloginfo( 'version' ), '<' )
		&& version_compare( $remote->requires_php, PHP_VERSION, '<' )
	) {
		
		$res = new stdClass();
		$res->slug = $remote->slug;
		$res->plugin = plugin_basename( __FILE__ ); // it could be just YOUR_PLUGIN_SLUG.php if your plugin doesn't have its own directory
		$res->new_version = $remote->version;
		$res->tested = $remote->tested;
		$res->package = $remote->download_url;
		$transient->response[ $res->plugin ] = $res;
		
		//$transient->checked[$res->plugin] = $remote->version;
	}
 
	return $transient;

}

And remember remember, if you need custom coding help, you can always get in touch with me and my team.

Misha Rudrastyh

Misha Rudrastyh

I develop websites since 2008, so it is total of 13 years of experience, oh my gosh. Most of all I love love love to create websites with WordPress and Gutenberg, some ideas and thoughts I share throughout my blog.

Need some developer help? Contact me