WordPress Plugin Licensing & Auto-Update System

WordPress Plugin Licensing & Auto-Update System – MinuteLaunch Skill

WordPress Plugin Licensing & Auto-Update System

Complete Implementation Guide Using FluentCart Pro

Production-Ready Solution

Overview

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:

┌────────────────────────────────────────────────────┐ │ SELLER SITE (your-site.com) │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ FluentCart Pro │ │ │ │ • Sells digital products │ │ │ │ • Generates licenses (status: inactive) │ │ │ └──────────────────────────────────────────────┘ │ │ ↓ │ │ ┌──────────────────────────────────────────────┐ │ │ │ SERVER PLUGIN (update-server.php) │ │ │ │ • REST API endpoints for validation │ │ │ │ • Activates licenses on first use │ │ │ │ • Serves plugin updates │ │ │ └──────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────┘ ↕ REST API ┌────────────────────────────────────────────────────┐ │ CUSTOMER SITE (Remote WordPress) │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ CLIENT PLUGIN (your-plugin.php) │ │ │ │ • Admin interface for activation │ │ │ │ • Calls server to validate license │ │ │ │ • Daily license checks │ │ │ │ • Automatic update checks & downloads │ │ │ └──────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────┘

Two Plugins Required

Plugin Location Purpose
Server Plugin
update-server.php
Seller site Validates licenses, serves updates, tracks activations
Client Plugin
your-plugin.php
Customer sites Activates licenses, checks for updates, downloads new versions

License Workflow

Step 1

Customer Purchases License

Location: Your seller site (checkout page)

  1. Customer selects product (1-site, 3-site, or 10-site license)
  2. Completes purchase via FluentCart Pro
  3. FluentCart generates license with status='inactive'
  4. Customer receives email with license key
Step 2

Customer Installs Plugin

Location: Customer’s WordPress site

  1. Downloads plugin ZIP from your site
  2. Uploads to WordPress: Plugins → Add New → Upload Plugin
  3. Activates plugin
  4. Plugin creates admin menu with “License Settings” page
Step 3

Customer Activates License

Location: Customer’s WordPress admin → Plugin Menu → License Settings

  1. Customer enters license key in form field
  2. Clicks “Activate License” button
  3. 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"
}
Step 4

Server Validates & Activates

Location: Your seller site (server plugin processes request)

  1. Finds license in database
  2. Validates: exists, not expired, not suspended, slots available
  3. Creates site record in database
  4. Creates activation record
  5. Changes license status from inactive to active
  6. Returns success response with activation count
Step 5

Client Stores License Data

Location: Customer’s WordPress site

  1. Stores license key in WordPress options
  2. Stores license status (active)
  3. Updates admin page to show “License Activated”
  4. Enables automatic updates
Step 6

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.

Step 7

Automatic Updates

Location: Customer’s WordPress site (checks every 12 hours)

  1. WordPress checks for plugin updates
  2. Client plugin queries server for new versions
  3. Server validates license and returns update info
  4. WordPress shows “Update available” notification
  5. Customer clicks “Update Now”
  6. WordPress downloads from server (license validated)
  7. Plugin updates automatically

Critical Implementation Details

Database Column Names

CRITICAL: FluentCart Pro’s database uses 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

IMPORTANT: FluentCart creates licenses with 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

CRITICAL: WordPress plugin ZIPs must be FLAT (files at root level, NO folder wrapper)

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
NEVER include the releases/ folder in your ZIP! This causes massive file bloat.

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

  1. WordPress checks for updates every 12 hours automatically
  2. Client plugin hooks in and calls your server API
  3. Server validates license and returns latest version info
  4. WordPress shows notification “Update available”
  5. Customer clicks “Update Now”
  6. Server validates again and serves ZIP file
  7. 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

  1. Update version numbers in plugin header and VERSION file
  2. Update CHANGELOG.md with changes
  3. Create clean ZIP (flat structure, NO releases folder)
  4. Upload ZIP to releases/ folder on your server
  5. 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)

Seller Site

On Your Seller Site

  1. Install and activate FluentCart Pro
  2. Configure payment gateway
  3. Create digital product for your plugin licenses
  4. Set pricing tiers (1-site, 3-site, 10-site)
  5. Upload and activate server plugin
  6. Verify REST API endpoints are registered
Customer Sites

Customer Installation Process

  1. Customer downloads plugin ZIP from your site
  2. Uploads to WordPress: Plugins → Add New → Upload Plugin
  3. Activates plugin
  4. Goes to License Settings page
  5. Enters license key and clicks “Activate”
  6. 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

  1. During update check: Server validates license before returning update info
  2. 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