WordPress Plugin Licensing & Auto-Update System
Complete Implementation Guide Using FluentCart Pro
Production-Ready SolutionOverview
This system enables selling WordPress plugins with license validation and automatic updates. Built on FluentCart Pro’s licensing module, it provides a complete solution for plugin distribution and license management.
What This System Does
- Sells licenses via FluentCart Pro on your seller site
- Validates licenses when customers activate on their sites
- Enforces activation limits (1, 3, or 10 sites per license)
- Delivers automatic updates from your server to customer sites
- Tracks license usage and expiration
Architecture
The system uses a two-plugin architecture:
Two Plugins Required
| Plugin | Location | Purpose |
|---|---|---|
Server Pluginupdate-server.php |
Seller site | Validates licenses, serves updates, tracks activations |
Client Pluginyour-plugin.php |
Customer sites | Activates licenses, checks for updates, downloads new versions |
License Workflow
Customer Purchases License
Location: Your seller site (checkout page)
- Customer selects product (1-site, 3-site, or 10-site license)
- Completes purchase via FluentCart Pro
- FluentCart generates license with
status='inactive' - Customer receives email with license key
Customer Installs Plugin
Location: Customer’s WordPress site
- Downloads plugin ZIP from your site
- Uploads to WordPress: Plugins → Add New → Upload Plugin
- Activates plugin
- Plugin creates admin menu with “License Settings” page
Customer Activates License
Location: Customer’s WordPress admin → Plugin Menu → License Settings
- Customer enters license key in form field
- Clicks “Activate License” button
- Client plugin calls server API endpoint
POST https://your-site.com/wp-json/fluent-cart/v1/license/activate
Body: {
"license_key": "YOUR_LICENSE_KEY",
"site_url": "https://customer-site.com"
}
Server Validates & Activates
Location: Your seller site (server plugin processes request)
- Finds license in database
- Validates: exists, not expired, not suspended, slots available
- Creates site record in database
- Creates activation record
- Changes license status from
inactivetoactive - Returns success response with activation count
Client Stores License Data
Location: Customer’s WordPress site
- Stores license key in WordPress options
- Stores license status (
active) - Updates admin page to show “License Activated”
- Enables automatic updates
Daily License Checks
Location: Customer’s WordPress site (runs daily via cron)
Client plugin checks license status daily to ensure it’s still valid, not expired, and activation is still active.
Automatic Updates
Location: Customer’s WordPress site (checks every 12 hours)
- WordPress checks for plugin updates
- Client plugin queries server for new versions
- Server validates license and returns update info
- WordPress shows “Update available” notification
- Customer clicks “Update Now”
- WordPress downloads from server (license validated)
- Plugin updates automatically
Critical Implementation Details
Database Column Names
site_url column, NOT url
Correct Implementation:
// CORRECT:
$site = LicenseSite::firstOrCreate(['site_url' => $site_url]);
$site = LicenseSite::where('site_url', $site_url)->first();
// WRONG (will cause SQL error):
$site = LicenseSite::firstOrCreate(['url' => $site_url]);
License Status Workflow
status='inactive' until first use
Correct Implementation:
// Accept inactive licenses (unused)
if ($license->status === 'inactive') {
$license->activate(); // Changes status to 'active'
}
// Only reject suspended or expired licenses
if ($license->status === 'suspended') {
return new WP_Error('suspended_license', 'License is suspended');
}
Plugin ZIP Structure
Correct PowerShell Script:
# Create temp folder
New-Item -ItemType Directory -Path "temp-flat" -Force
# Copy ONLY essential files (NO releases folder!)
Copy-Item "your-plugin.php" "temp-flat\"
Copy-Item "VERSION" "temp-flat\"
Copy-Item "CHANGELOG.md" "temp-flat\"
# Compress contents (use \* wildcard for flat structure)
Compress-Archive -Path "temp-flat\*" `
-DestinationPath "your-plugin-v1.0.X.zip" -Force
# Clean up
Remove-Item "temp-flat" -Recurse -Force
Server Plugin Implementation
REST API Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/fluent-cart/v1/license/activate |
POST | Activate license for a site |
/fluent-cart/v1/license/check |
POST | Daily validation of license status |
/fluent-cart/v1/license/deactivate |
POST | Remove activation from a site |
/fluentcart/v1/plugin-update-check |
POST | Check if new version available |
/fluentcart/v1/plugin-download |
GET | Download plugin ZIP (license validated) |
License Activation Handler
function handle_license_activate($request) {
$license_key = $request->get_param('license_key');
$site_url = $request->get_param('site_url');
// Find license
$license = License::where('license_key', $license_key)->first();
if (!$license) {
return new WP_Error('invalid_license', 'Invalid license key');
}
// Validate
if ($license->isExpired()) {
return new WP_Error('expired_license', 'License has expired');
}
if ($license->status === 'suspended') {
return new WP_Error('suspended_license', 'License is suspended');
}
// Check activation limit
$current_activations = LicenseActivation::where('license_id', $license->id)
->where('status', 'active')
->count();
if ($license->limit > 0 && $current_activations >= $license->limit) {
return new WP_Error('activation_limit_reached',
'Activation limit reached');
}
// Create site (CRITICAL: use 'site_url' not 'url')
$site = LicenseSite::firstOrCreate(['site_url' => $site_url]);
// Create activation
LicenseActivation::create([
'site_id' => $site->id,
'license_id' => $license->id,
'status' => 'active'
]);
// Activate license on first use
if ($license->status === 'inactive') {
$license->activate();
}
return rest_ensure_response([
'success' => true,
'message' => 'License activated successfully',
'data' => [
'license_key' => $license_key,
'status' => 'active',
'activation_count' => $current_activations + 1,
'activation_limit' => $license->limit
]
]);
}
Client Plugin Implementation
License Activation (Admin Interface)
function handle_license_activation() {
$license_key = sanitize_text_field($_POST['license_key']);
$site_url = home_url();
$response = wp_remote_post(
'https://your-site.com/wp-json/fluent-cart/v1/license/activate',
array(
'body' => json_encode(array(
'license_key' => $license_key,
'site_url' => $site_url
)),
'headers' => array('Content-Type' => 'application/json')
)
);
$body = json_decode(wp_remote_retrieve_body($response), true);
if ($body['success']) {
update_option('your_plugin_license_key', $license_key);
update_option('your_plugin_license_status', 'active');
update_option('your_plugin_license_data', $body['data']);
}
}
Daily License Check (Cron)
function daily_license_check() {
$license_key = get_option('your_plugin_license_key');
$response = wp_remote_post(
'https://your-site.com/wp-json/fluent-cart/v1/license/check',
array(
'body' => json_encode(array(
'license_key' => $license_key,
'site_url' => home_url()
)),
'headers' => array('Content-Type' => 'application/json')
)
);
$body = json_decode(wp_remote_retrieve_body($response), true);
update_option('your_plugin_license_status', $body['data']['status']);
update_option('your_plugin_license_last_check', time());
}
// Register cron event
add_action('your_plugin_daily_license_check', 'daily_license_check');
Update Check (WordPress Hook)
function check_for_updates($transient) {
if (empty($transient->checked)) {
return $transient;
}
$license_key = get_option('your_plugin_license_key');
$plugin_data = get_plugin_data(__FILE__);
$response = wp_remote_post(
'https://your-site.com/wp-json/fluentcart/v1/plugin-update-check',
array(
'body' => json_encode(array(
'plugin_slug' => 'your-plugin',
'current_version' => $plugin_data['Version'],
'license_key' => $license_key,
'domain' => home_url()
)),
'headers' => array('Content-Type' => 'application/json')
)
);
$update_data = json_decode(wp_remote_retrieve_body($response), true);
if (version_compare($plugin_data['Version'], $update_data['version'], '<')) {
$transient->response['your-plugin/your-plugin.php'] = (object) array(
'slug' => 'your-plugin',
'new_version' => $update_data['version'],
'package' => $update_data['download_url'],
'url' => 'https://your-site.com'
);
}
return $transient;
}
add_filter('pre_set_site_transient_update_plugins', 'check_for_updates');
Auto-Update System
How It Works
- WordPress checks for updates every 12 hours automatically
- Client plugin hooks in and calls your server API
- Server validates license and returns latest version info
- WordPress shows notification “Update available”
- Customer clicks “Update Now”
- Server validates again and serves ZIP file
- WordPress installs automatically
Where Updates Are Stored
On your seller site, store plugin ZIPs in a releases/ folder:
plugin-files/
your-plugin/
releases/
your-plugin-v1.0.1.zip
your-plugin-v1.0.2.zip
your-plugin-v1.0.3.zip ← Latest version
The server plugin automatically finds the highest version number in this folder.
Releasing a New Version
- Update version numbers in plugin header and VERSION file
- Update CHANGELOG.md with changes
- Create clean ZIP (flat structure, NO releases folder)
- Upload ZIP to
releases/folder on your server - Done! Server automatically detects and serves it
No database updates needed. No customer notifications needed. Completely automatic!
Database Schema
FluentCart Pro Tables Used
wp_fc_licenses
CREATE TABLE `wp_fc_licenses` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`license_key` varchar(255) NOT NULL,
`status` varchar(50) DEFAULT 'inactive',
`limit` int(11) DEFAULT 0,
`activation_count` int(11) DEFAULT 0,
`expiration_date` datetime DEFAULT NULL,
`product_id` bigint(20) unsigned,
`created_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `license_key` (`license_key`)
);
wp_fct_license_sites
CREATE TABLE `wp_fct_license_sites` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`site_url` varchar(255) NOT NULL, -- CRITICAL: 'site_url' not 'url'
`created_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `site_url` (`site_url`)
);
wp_fct_license_activations
CREATE TABLE `wp_fct_license_activations` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`license_id` bigint(20) unsigned NOT NULL,
`site_id` bigint(20) unsigned NOT NULL,
`status` varchar(50) DEFAULT 'active',
`created_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `license_id` (`license_id`),
KEY `site_id` (`site_id`)
);
Testing Checklist
Test License Activation
- Fresh license (inactive) activates successfully
- Reactivate same site succeeds with “reactivated” message
- Exceed activation limit fails with limit error
- Expired license fails with expiration error
Test Auto-Updates
- Check for updates in WordPress admin
- Update notification appears when new version available
- Click “Update Now” downloads from your server
- Plugin updates successfully
- License remains active after update
Test License Deactivation
- Deactivate license from admin interface
- Activation slot freed up
- Can activate on different site
Deployment
Initial Setup (One-Time)
On Your Seller Site
- Install and activate FluentCart Pro
- Configure payment gateway
- Create digital product for your plugin licenses
- Set pricing tiers (1-site, 3-site, 10-site)
- Upload and activate server plugin
- Verify REST API endpoints are registered
Customer Installation Process
- Customer downloads plugin ZIP from your site
- Uploads to WordPress: Plugins → Add New → Upload Plugin
- Activates plugin
- Goes to License Settings page
- Enters license key and clicks “Activate”
- Sees “License Activated” confirmation
Success Criteria
The system is working correctly when all of these are true:
- Customers can purchase licenses on your seller site
- Licenses are created with status=’inactive’
- Customers can activate licenses on their sites
- First activation changes license status to ‘active’
- Activation limits are enforced
- Daily license checks run successfully
- Update notifications appear in WordPress admin
- Updates download and install automatically
- License remains active after updates
- Deactivation frees up activation slot
Key Takeaways
Critical Lessons
- Always check database schema before writing queries
- Understand the workflow (inactive → active on first use)
- Test with real data (inactive licenses, activation limits)
- Use flat ZIP structure (files at root, NO releases/)
- Document as you go (saves hours of debugging)
What Works Well
- Two-plugin architecture: Clean separation of concerns
- FluentCart integration: Leverages existing license models
- WordPress hooks: Uses standard update system
- Automatic everything: No manual distribution needed
Security Considerations
License Validation Happens Twice
- During update check: Server validates license before returning update info
- During download: Server validates license again before serving ZIP
This prevents expired licenses, deactivated sites, and invalid keys from getting updates.
Best Practices
- Always use HTTPS for all API endpoints
- Sanitize and validate all input parameters
- Use WordPress nonce verification for admin actions
- Store license keys in WordPress options (not publicly accessible)
- Log failed activation attempts for security monitoring
