# GitHub Auto-Updates for WordPress Plugins
Complete guide for implementing automatic updates for WordPress plugins hosted outside the WordPress.org repository.
---
## Table of Contents
1. [Overview](#overview)
2. [How WordPress Updates Work](#how-wordpress-updates-work)
3. [Solution 1: Plugin Update Checker (Recommended)](#solution-1-plugin-update-checker-recommended)
4. [Solution 2: Git Updater](#solution-2-git-updater)
5. [Solution 3: Custom Update Server](#solution-3-custom-update-server)
6. [Commercial Solutions](#commercial-solutions)
7. [Security Best Practices](#security-best-practices)
8. [Comparison Matrix](#comparison-matrix)
9. [Common Pitfalls](#common-pitfalls)
10. [Resources](#resources)
---
## Overview
WordPress plugins don't need to be hosted on WordPress.org to provide automatic updates. Several well-established solutions enable auto-updates from GitHub, GitLab, BitBucket, or custom servers.
### Why Auto-Updates Matter
- **Security**: Users get critical security patches automatically
- **Features**: Seamless delivery of new features and improvements
- **Support**: Reduces support burden from outdated installations
- **Professional**: Provides polished user experience
### Solutions Available
| Solution | Best For | Complexity | Cost |
|----------|----------|------------|------|
| Plugin Update Checker | Most use cases | Low | Free |
| Git Updater | Multi-platform Git hosting | Low | Free |
| Custom Update Server | Enterprise/custom licensing | High | Hosting costs |
| Freemius | Commercial plugins | Low | 15% or $99-599/yr |
---
## How WordPress Updates Work
### Core Update Mechanism
WordPress uses a **transient-based system** to check for plugin updates:
```
1. WordPress checks transients (every 12 hours)
2. Queries WordPress.org API for available updates
3. Stores results in `update_plugins` transient
4. Displays "Update available" notifications
5. Downloads and installs when user clicks "Update"
```
### Key Hooks for Custom Updates
```php
// Intercept BEFORE WordPress checks (saves to transient)
add_filter('pre_set_site_transient_update_plugins', 'my_check_for_updates');
// Filter update data (returns without saving)
add_filter('site_transient_update_plugins', 'my_push_update');
// Modify plugin information for "View details" modal
add_filter('plugins_api', 'my_plugin_info', 20, 3);
// Post-installation cleanup
add_action('upgrader_post_install', 'my_post_install', 10, 3);
```
### Update Flow for Custom Plugins
```
1. WordPress checks transients
↓
2. Your filter hook intercepts
↓
3. You query GitHub/custom server
↓
4. Inject update data into transient
↓
5. WordPress shows "Update available"
↓
6. User clicks update
↓
7. WordPress downloads from your URL
↓
8. Installs and activates
```
---
## Solution 1: Plugin Update Checker (Recommended)
**GitHub**: https://github.com/YahnisElsts/plugin-update-checker
**Stars**: 2.2k+ | **License**: MIT | **Status**: Actively maintained (v5.x)
### What It Does
Lightweight library that enables automatic updates from GitHub, GitLab, BitBucket, or custom JSON servers. No WordPress.org submission required.
### Pros
- ✅ **Minimal setup**: ~5 lines of code
- ✅ **Multiple platforms**: GitHub, GitLab, BitBucket, custom JSON
- ✅ **Active development**: Regular updates since 2011
- ✅ **Private repos**: Supports authentication tokens
- ✅ **Well documented**: Extensive examples and guides
- ✅ **No dependencies**: Self-contained library
- ✅ **Flexible versioning**: Supports releases, tags, or branches
- ✅ **Standards compliant**: Uses WordPress.org `readme.txt` format
### Cons
- ❌ No built-in license management (requires custom implementation)
- ❌ No package signing (relies on HTTPS/token security)
- ❌ Requires bundling library with plugin (~100KB)
### Installation
**Option A: Git Submodule** (Recommended)
```bash
cd your-plugin/
git submodule add https://github.com/YahnisElsts/plugin-update-checker.git
```
**Option B: Composer**
```bash
composer require yahnis-elsts/plugin-update-checker
```
**Option C: Manual**
```bash
cd your-plugin/
git clone https://github.com/YahnisElsts/plugin-update-checker.git
# Or download and extract ZIP
```
### Basic Implementation
```php
setBranch('main');
```
### Private Repository Support
```php
// For private GitHub repos, add authentication
$myUpdateChecker->setAuthentication('ghp_YourGitHubPersonalAccessToken');
// Better: Use WordPress constant (define in wp-config.php)
if (defined('MY_PLUGIN_GITHUB_TOKEN')) {
$myUpdateChecker->setAuthentication(MY_PLUGIN_GITHUB_TOKEN);
}
```
### Release Strategies
#### Strategy 1: GitHub Releases (Recommended)
**Best for**: Production plugins with formal releases
```bash
# 1. Update version in plugin header
# my-plugin.php: Version: 1.0.1
# 2. Commit and tag
git add my-plugin.php
git commit -m "Bump version to 1.0.1"
git tag 1.0.1
git push origin main
git push origin 1.0.1
# 3. Create GitHub Release
# - Go to GitHub → Releases → Create Release
# - Select tag: 1.0.1
# - Upload pre-built plugin ZIP (optional but recommended)
```
**Enable release assets**:
```php
// Download ZIP from releases instead of source code
$myUpdateChecker->getVcsApi()->enableReleaseAssets();
```
**Benefits**:
- ✅ Professional release notes
- ✅ Pre-built ZIP (can include compiled assets)
- ✅ Changelog visible to users
- ✅ Can exclude dev files (.git, tests, etc.)
#### Strategy 2: Git Tags
**Best for**: Simple plugins, rapid iteration
```bash
# Just tag the commit
git tag 1.0.1
git push origin 1.0.1
```
**No additional code needed** - library auto-detects highest version tag.
**Benefits**:
- ✅ Simple workflow
- ✅ No manual release creation
**Drawbacks**:
- ❌ Downloads entire repo (includes .git, tests, etc.)
- ❌ No release notes visible to users
#### Strategy 3: Branch-Based
**Best for**: Beta testing, staging environments
```php
// Point to specific branch
$myUpdateChecker->setBranch('stable');
// Or beta branch
$myUpdateChecker->setBranch('beta');
```
**Update version in plugin header on that branch**:
```php
/**
* Version: 1.1.0-beta
*/
```
**Benefits**:
- ✅ Easy beta testing
- ✅ Separate stable/development channels
**Drawbacks**:
- ❌ No version history
- ❌ Downloads full repo
### Complete Example with Best Practices
```php
';
echo 'My Plugin: Update checker library not found. ';
echo 'Automatic updates disabled.';
echo '
';
});
return;
}
require $updater_path;
$updateChecker = YahnisElsts\PluginUpdateChecker\v5\PucFactory::buildUpdateChecker(
'https://github.com/yourusername/my-plugin/',
MY_PLUGIN_FILE,
'my-plugin'
);
// Set branch
$updateChecker->setBranch('main');
// Use GitHub Releases
$updateChecker->getVcsApi()->enableReleaseAssets();
// Private repo authentication (optional)
if (defined('MY_PLUGIN_GITHUB_TOKEN')) {
$updateChecker->setAuthentication(MY_PLUGIN_GITHUB_TOKEN);
}
// Add custom authentication from settings (optional)
$license_key = get_option('my_plugin_license_key');
if (!empty($license_key)) {
// Use license key as token or validate separately
$updateChecker->setAuthentication($license_key);
}
}
// Rest of plugin code...
```
### Creating Update-Ready ZIP Files
**Important**: ZIP must contain plugin folder inside it!
```
my-plugin-1.0.1.zip
└── my-plugin/ ← Plugin folder MUST be inside
├── my-plugin.php
├── readme.txt
├── plugin-update-checker/
└── ...
```
**Build script example**:
```bash
#!/bin/bash
# build-release.sh
VERSION="1.0.1"
PLUGIN_SLUG="my-plugin"
# Create temp directory
mkdir -p build
# Export git repository (excludes .git, .gitignore, etc.)
git archive HEAD --prefix="${PLUGIN_SLUG}/" --format=zip -o "build/${PLUGIN_SLUG}-${VERSION}.zip"
echo "Built: build/${PLUGIN_SLUG}-${VERSION}.zip"
```
**With exclusions**:
```bash
# .gitattributes
.git export-ignore
.gitignore export-ignore
.gitattributes export-ignore
tests/ export-ignore
node_modules/ export-ignore
src/ export-ignore
.github/ export-ignore
```
### readme.txt Format
Plugin Update Checker reads `readme.txt` for changelog and upgrade notices:
```
=== My Plugin ===
Contributors: yourusername
Tags: feature, awesome
Requires at least: 5.9
Tested up to: 6.4
Stable tag: 1.0.1
License: GPLv2 or later
Short description here.
== Description ==
Long description here.
== Changelog ==
= 1.0.1 =
* Fixed bug with user permissions
* Added new feature X
= 1.0.0 =
* Initial release
== Upgrade Notice ==
= 1.0.1 =
Critical security fix. Update immediately.
```
---
## Solution 2: Git Updater
**GitHub**: https://github.com/afragen/git-updater
**Stars**: 3.3k+ | **License**: MIT | **Status**: Actively maintained
### What It Does
A **WordPress plugin** (not library) that enables updates for **all** GitHub/GitLab/Bitbucket/Gitea-hosted plugins and themes on a site.
### Pros
- ✅ **User-installable**: Site owners install once, works for all compatible plugins
- ✅ **Multi-platform**: GitHub, GitLab, Bitbucket, Gitea
- ✅ **No coding required**: Just add headers to plugin
- ✅ **Language pack support**: Automatic translations
- ✅ **REST API**: Programmatic control
### Cons
- ❌ **Dependency**: Users must install Git Updater plugin first
- ❌ **Less control**: Developer doesn't control update logic
- ❌ **PHP 8.0+ required**: May limit compatibility
### Implementation
**Step 1: Add headers to your plugin**
```php
Installation- Upload plugin
",
"changelog": "1.0.1
"
},
"banners": {
"low": "https://example.com/banner-772x250.png",
"high": "https://example.com/banner-1544x500.png"
},
"icons": {
"1x": "https://example.com/icon-128x128.png",
"2x": "https://example.com/icon-256x256.png"
}
}
```
#### Plugin Code
```php
checked)) {
return $transient;
}
$plugin_slug = plugin_basename(__FILE__);
// Check cache first (12 hours)
$remote_data = get_transient('my_plugin_update_cache');
if (false === $remote_data) {
$remote = wp_remote_get(MY_PLUGIN_UPDATE_URL, [
'timeout' => 10,
'headers' => [
'Accept' => 'application/json'
]
]);
if (is_wp_error($remote) || 200 !== wp_remote_retrieve_response_code($remote)) {
return $transient;
}
$remote_data = json_decode(wp_remote_retrieve_body($remote));
if (!$remote_data || !isset($remote_data->version)) {
return $transient;
}
// Cache for 12 hours
set_transient('my_plugin_update_cache', $remote_data, 12 * HOUR_IN_SECONDS);
}
// Compare versions
if (version_compare(MY_PLUGIN_VERSION, $remote_data->version, '<')) {
$obj = new stdClass();
$obj->slug = $remote_data->slug;
$obj->plugin = $plugin_slug;
$obj->new_version = $remote_data->version;
$obj->url = $remote_data->homepage;
$obj->package = $remote_data->download_url;
$obj->tested = $remote_data->tested;
$obj->requires = $remote_data->requires;
$obj->requires_php = $remote_data->requires_php;
$transient->response[$plugin_slug] = $obj;
} else {
// Mark as up-to-date
$obj = new stdClass();
$obj->slug = $remote_data->slug;
$obj->plugin = $plugin_slug;
$obj->new_version = $remote_data->version;
$transient->no_update[$plugin_slug] = $obj;
}
return $transient;
}
/**
* Plugin information for "View details" modal
*/
add_filter('plugins_api', 'my_plugin_info', 20, 3);
function my_plugin_info($res, $action, $args) {
// Do nothing if not getting plugin information
if ('plugin_information' !== $action) {
return $res;
}
// Do nothing if it's not our plugin
if (plugin_basename(__DIR__) !== $args->slug) {
return $res;
}
// Try to get cached data
$remote_data = get_transient('my_plugin_update_cache');
if (false === $remote_data) {
$remote = wp_remote_get(MY_PLUGIN_UPDATE_URL, [
'timeout' => 10,
'headers' => ['Accept' => 'application/json']
]);
if (is_wp_error($remote) || 200 !== wp_remote_retrieve_response_code($remote)) {
return $res;
}
$remote_data = json_decode(wp_remote_retrieve_body($remote));
if (!$remote_data) {
return $res;
}
}
$res = new stdClass();
$res->name = $remote_data->name;
$res->slug = $remote_data->slug;
$res->version = $remote_data->version;
$res->tested = $remote_data->tested;
$res->requires = $remote_data->requires;
$res->requires_php = $remote_data->requires_php;
$res->author = $remote_data->author;
$res->homepage = $remote_data->homepage;
$res->download_link = $remote_data->download_url;
$res->sections = (array)$remote_data->sections;
if (isset($remote_data->banners)) {
$res->banners = (array)$remote_data->banners;
}
if (isset($remote_data->icons)) {
$res->icons = (array)$remote_data->icons;
}
if (isset($remote_data->last_updated)) {
$res->last_updated = $remote_data->last_updated;
}
return $res;
}
/**
* Clear cache when plugin is updated
*/
add_action('upgrader_process_complete', 'my_plugin_clear_update_cache', 10, 2);
function my_plugin_clear_update_cache($upgrader_object, $options) {
if ('update' === $options['action'] && 'plugin' === $options['type']) {
delete_transient('my_plugin_update_cache');
}
}
```
### With License Validation
```php
// Add license key field in settings
function my_plugin_check_for_updates($transient) {
$license_key = get_option('my_plugin_license_key');
if (empty($license_key)) {
return $transient; // No license = no updates
}
$remote = wp_remote_post(MY_PLUGIN_UPDATE_URL, [
'body' => [
'license' => $license_key,
'domain' => home_url()
]
]);
// Verify license is valid before offering update
$remote_data = json_decode(wp_remote_retrieve_body($remote));
if (!isset($remote_data->license_valid) || !$remote_data->license_valid) {
return $transient; // Invalid license = no updates
}
// Proceed with update check...
}
```
### When to Use
- ✅ Need full control over update logic
- ✅ Want to integrate license verification
- ✅ Building commercial plugin with custom licensing
- ✅ Need analytics/tracking on updates
- ✅ Want to host on own infrastructure
---
## Commercial Solutions
### Freemius
**Website**: https://freemius.com
**Pricing**: Free (15% revenue share) or $99-599/year
#### Features
- ✅ Complete platform: Licensing, payments, updates, analytics
- ✅ WordPress SDK: Drop-in library
- ✅ Automatic updates with license integration
- ✅ In-dashboard checkout: ~12% conversion boost
- ✅ Secure repository: Amazon S3-backed
- ✅ Staged rollouts: Beta testing, gradual releases
#### Implementation
```php
// Include Freemius SDK
require_once dirname(__FILE__) . '/freemius/start.php';
$my_plugin_fs = fs_dynamic_init([
'id' => '123',
'slug' => 'my-plugin',
'public_key' => 'pk_xxx',
'is_premium' => true,
'has_paid_plans' => true,
'menu' => [
'slug' => 'my-plugin',
],
]);
```
Updates are fully automatic - Freemius handles everything.
#### When to Use
- ✅ Selling premium plugins commercially
- ✅ Want all-in-one solution (licensing + updates + payments)
- ✅ Need subscription management
- ✅ Okay with revenue share
---
### Easy Digital Downloads + Software Licensing
**Website**: https://easydigitaldownloads.com
**Pricing**: $328/year (Recurring Payments + Software Licensing extensions)
#### Features
- ✅ Self-hosted: Run on your own WordPress site
- ✅ Full control: Own the data
- ✅ No revenue share: Fixed annual cost
- ✅ Mature ecosystem: 10+ years
#### Limitations
- ❌ More setup: Requires custom development for update integration
- ❌ No in-dashboard checkout
- ❌ Extension costs add up
#### When to Use
- ✅ Already using WordPress for sales
- ✅ Want complete control over infrastructure
- ✅ Have development resources
- ❌ Don't want to build update logic (use Freemius instead)
---
## Security Best Practices
### 1. Always Use HTTPS
```php
// ✅ Good
$remote = wp_remote_get('https://example.com/updates.json', [
'sslverify' => true // Explicitly verify SSL
]);
// ❌ Bad
$remote = wp_remote_get('http://example.com/updates.json');
```
**Why**: HTTPS prevents man-in-the-middle attacks.
### 2. Implement Token Authentication
**For private repos**:
```php
$updateChecker->setAuthentication(defined('MY_PLUGIN_TOKEN') ? MY_PLUGIN_TOKEN : '');
```
**For custom servers**:
```php
$remote = wp_remote_get($update_url, [
'headers' => [
'Authorization' => 'Bearer ' . get_option('my_plugin_license_key')
]
]);
```
**Never hardcode tokens** - use constants or encrypted options.
### 3. Validate License Keys
```php
function my_plugin_check_for_updates($transient) {
$license = get_option('my_plugin_license');
// Validate license before offering updates
$validation = wp_remote_post('https://example.com/api/validate', [
'body' => [
'license' => $license,
'domain' => home_url()
]
]);
$license_data = json_decode(wp_remote_retrieve_body($validation));
if (!$license_data->valid) {
return $transient; // No updates for invalid licenses
}
// Proceed...
}
```
### 4. Use Checksums
**Server** (info.json):
```json
{
"download_url": "https://example.com/plugin.zip",
"checksum": "sha256:abc123def456...",
"checksum_algorithm": "sha256"
}
```
**Plugin**:
```php
add_filter('upgrader_pre_install', 'my_plugin_verify_checksum', 10, 2);
function my_plugin_verify_checksum($true, $hook_extra) {
$package = $hook_extra['package'];
// Get expected checksum
$expected = get_transient('my_plugin_expected_checksum');
if (!$expected) {
return $true; // Allow if no checksum available
}
// Download and verify
$downloaded = download_url($package);
if (is_wp_error($downloaded)) {
return $downloaded;
}
$actual = hash_file('sha256', $downloaded);
if (!hash_equals($expected, $actual)) {
@unlink($downloaded);
return new WP_Error('checksum_mismatch', 'Update file corrupted or tampered');
}
return $true;
}
```
### 5. Implement Package Signing (Advanced)
**Server-side** (sign ZIP):
```php
$zip_contents = file_get_contents('plugin.zip');
$private_key = openssl_pkey_get_private(file_get_contents('private.pem'));
openssl_sign($zip_contents, $signature, $private_key, OPENSSL_ALGO_SHA256);
$info = [
'download_url' => 'https://example.com/plugin.zip',
'signature' => base64_encode($signature)
];
file_put_contents('info.json', json_encode($info));
```
**Plugin-side** (verify):
```php
$remote_data = json_decode(wp_remote_retrieve_body($remote));
$zip = file_get_contents($remote_data->download_url);
$signature = base64_decode($remote_data->signature);
// Embed public key in plugin
$public_key = <<get_error_message()
));
return $transient;
}
// Continue...
}
```
### 8. Secure Token Storage
```php
// ❌ Bad: Hardcoded
$token = 'ghp_abc123';
// ✅ Good: WordPress constant (wp-config.php)
define('MY_PLUGIN_GITHUB_TOKEN', 'ghp_xxx');
$token = MY_PLUGIN_GITHUB_TOKEN;
// ✅ Better: Encrypted option
function my_plugin_get_token() {
$encrypted = get_option('my_plugin_token_encrypted');
return openssl_decrypt($encrypted, 'AES-256-CBC', wp_salt('auth'));
}
```
---
## Comparison Matrix
| Feature | Plugin Update Checker | Git Updater | Custom Server | Freemius |
|---------|----------------------|-------------|---------------|----------|
| **Setup Complexity** | ⭐⭐⭐⭐⭐ Low | ⭐⭐⭐⭐ Low | ⭐⭐ High | ⭐⭐⭐⭐⭐ Low |
| **Cost** | Free | Free | Hosting costs | 15% or $99-599/yr |
| **GitHub Support** | ✅ Yes | ✅ Yes | ✅ (DIY) | ❌ No |
| **Private Repos** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **License Management** | ❌ DIY | ❌ No | ✅ DIY | ✅ Built-in |
| **Package Signing** | ❌ No | ❌ No | ✅ DIY | ✅ Built-in |
| **HTTPS Required** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Dependencies** | Library (bundled) | Plugin (separate) | None | SDK (bundled) |
| **Control Level** | Medium | Low | High | Medium |
| **Maintenance** | Low | Low | High | None |
| **Scalability** | High (GitHub CDN) | High | Varies | High (S3) |
| **In-Dashboard Checkout** | ❌ No | ❌ No | ✅ DIY | ✅ Yes |
| **Analytics** | ❌ No | ❌ No | ✅ DIY | ✅ Built-in |
| **Best For** | Open source, freemium | Multi-platform | Custom licensing | Commercial SaaS |
---
## Common Pitfalls
### 1. Incorrect ZIP Structure
❌ **Wrong**:
```
plugin.zip
├── plugin.php
├── readme.txt
└── assets/
```
✅ **Correct**:
```
plugin.zip
└── my-plugin/ ← Plugin folder MUST be inside
├── plugin.php
├── readme.txt
└── assets/
```
**Fix**: Always include plugin folder in ZIP.
### 2. Missing HTTPS
```php
// ❌ Bad
$url = 'http://example.com/updates.json';
// ✅ Good
$url = 'https://example.com/updates.json';
```
**Fix**: Always use HTTPS for update URLs.
### 3. Not Caching Requests
```php
// ❌ Bad: Checks every page load
$remote = wp_remote_get($update_url);
// ✅ Good: Cache for 12 hours
$cached = get_transient('my_plugin_update_data');
if (false === $cached) {
$remote = wp_remote_get($update_url);
$cached = wp_remote_retrieve_body($remote);
set_transient('my_plugin_update_data', $cached, 12 * HOUR_IN_SECONDS);
}
```
**Fix**: Always cache remote requests.
### 4. Hardcoded Tokens
```php
// ❌ Bad
$updateChecker->setAuthentication('ghp_abc123');
// ✅ Good
if (defined('MY_PLUGIN_TOKEN')) {
$updateChecker->setAuthentication(MY_PLUGIN_TOKEN);
}
```
**Fix**: Use constants or encrypted options.
### 5. No Error Handling
```php
// ❌ Bad
$remote = wp_remote_get($url);
$data = json_decode(wp_remote_retrieve_body($remote));
// ✅ Good
$remote = wp_remote_get($url);
if (is_wp_error($remote)) {
error_log('Update check failed: ' . $remote->get_error_message());
return $transient;
}
if (200 !== wp_remote_retrieve_response_code($remote)) {
error_log('Update check returned: ' . wp_remote_retrieve_response_code($remote));
return $transient;
}
$data = json_decode(wp_remote_retrieve_body($remote));
if (!$data || !isset($data->version)) {
error_log('Invalid update data received');
return $transient;
}
```
**Fix**: Always validate responses.
### 6. Version Comparison Issues
```php
// ❌ Bad: String comparison
if (MY_PLUGIN_VERSION < $remote_data->version) {
// Fails: "1.10.0" < "1.2.0" = false (string comparison)
}
// ✅ Good: Semantic version comparison
if (version_compare(MY_PLUGIN_VERSION, $remote_data->version, '<')) {
// Correctly: "1.10.0" < "1.2.0" = false (semantic comparison)
}
```
**Fix**: Use `version_compare()`.
### 7. Forgetting to Flush Cache
```php
// After updating plugin, clear cached update data
add_action('upgrader_process_complete', 'my_plugin_clear_cache');
function my_plugin_clear_cache() {
delete_transient('my_plugin_update_cache');
}
```
**Fix**: Clear transients after updates.
---
## Resources
### Official Documentation
- **Plugin Update Checker**: https://github.com/YahnisElsts/plugin-update-checker
- **Git Updater**: https://github.com/afragen/git-updater/wiki
- **WordPress Plugin API**: https://developer.wordpress.org/plugins/
- **Transients API**: https://developer.wordpress.org/apis/transients/
### Tutorials
- **Self-Hosted Updates**: https://rudrastyh.com/wordpress/self-hosted-plugin-update.html
- **GitHub Updates**: https://code.tutsplus.com/tutorials/distributing-your-plugins-in-github-with-automatic-updates--wp-34817
- **Serverless Update Server**: https://macarthur.me/posts/serverless-wordpress-plugin-update-server/
### Security
- **WordPress Security**: https://developer.wordpress.org/plugins/wordpress-org/plugin-security/
- **Checksum Verification**: https://developer.wordpress.org/cli/commands/plugin/verify-checksums/
- **Package Signing Proposal**: https://core.trac.wordpress.org/ticket/39309
### Commercial Platforms
- **Freemius**: https://freemius.com
- **EDD Software Licensing**: https://easydigitaldownloads.com/downloads/software-licensing/
---
**Recommended**: For most developers, use **Plugin Update Checker** with GitHub. It provides the best balance of simplicity, features, and security.
**Next Steps**:
1. Choose your update strategy
2. See `examples/github-updater.php` for complete working example
3. Implement in your plugin
4. Test thoroughly before releasing
---
**Last Updated**: 2025-11-06
**Version**: 1.0.0