Overview

This tutorial is for Joomla extension developers who run their own copy of Multizone Subscriptions Manager (pkg_subsmgr) to manage tiered subscriptions for their customers. You will learn how to build a customer-side extension that validates subscription keys against your own Subscriptions Manager installation and enforces per-tier feature limits.

Audience: Joomla 4.x, 5.x, and 6.x extension developers who want to offer their extensions with tier-based feature gating (Trial, Standard, Premium, Enterprise) without building their own subscription infrastructure.

Signed Validation

RSA-2048 signed responses bound to the customer's Joomla installation fingerprint, preventing cache copying between sites.

Admin subscription key edit view showing the baked-in tier_limits JSON and signature fields
Admin subscription key edit view showing the baked-in tier_limits JSON and signature fields
Tier-Based Limits

Four feature types — cumulative, periodic, boolean, tiered value — let you gate anything from row counts to monthly API quotas.

Product Features list, filtered to one product (com_veriform)
Product Features list, filtered to one product (com_veriform)
Graceful Degradation

Cached responses keep customers running when your validation server is unreachable, falling back to trial tier only as a last resort.

Dashboard with Active Subscription panel showing tier + expiry
Dashboard with Active Subscription panel showing tier + expiry

How it Works

There are two sites involved: yours (running Subscriptions Manager, acting as the subscription authority) and your customer's (running your extension). Your extension talks to your Subscriptions Manager via its REST API.

  1. You define product features in your Subscriptions Manager admin — one row per feature, with a value for each tier.
  2. You issue a subscription key to your customer. Tier limits are baked into the key as a signed JSON snapshot at creation time.
  3. Your extension (on the customer's site) validates the key against your Subscriptions Manager's public API. The response is RSA-signed and bound to the customer's Joomla secret, so it cannot be copied between sites.
  4. Your extension enforces limits locally using a cached copy of the response. You call checkResourceLimit('feature_key') before allowing an action.

Prerequisites

  • Subscriptions Manager (pkg_subsmgr) installed on your own Joomla site, configured as a subscription authority.
  • A Joomla extension of your own with at least one resource worth gating (rows in a table, API calls, stored files, email sends, etc.).
  • The RSA public key generated by your Subscriptions Manager installation (required by customer-side extensions to verify signed responses).
  • A configuration field in your extension where customers can paste their subscription key.

Define Your Product Features

In your Subscriptions Manager admin, go to Components > Subscriptions Manager > Product Features and add a row per feature you want to gate. Example for a hypothetical article generator:

feature_keytypetrialstandardpremiumenterprise
max_articles cumulative 5 100 1000 -1
api_calls_monthly periodic 100 10,000 100,000 -1
custom_templates boolean 0 1 1 1
export_formats tiered_value 1 3 5 10

Feature types

  • cumulative — total active count. Block when at limit. -1 means unlimited, 0 means blocked entirely.
  • periodic — monthly resetting counter (resets on the 1st). Useful for API call quotas.
  • boolean — feature on/off flag. 1 enables, 0 disables.
  • tiered_value — numeric caps that scale per tier (e.g. batch size limit).
Product Feature Details
Product Feature Tier Values

Extension Architecture

Add a TierService class to your extension's admin Service/ folder. It uses two reusable traits shipped with pkg_subsmgr that you copy into your extension:

  • SubscriptionValidationTrait — handles API calls, signature verification, caching, and fallback.
  • LimitEnforcementTrait — registers resources and enforces limits.
namespace YourVendor\Component\YourExt\Administrator\Service;

use YourVendor\Component\YourExt\Administrator\Trait\SubscriptionValidationTrait;
use YourVendor\Component\YourExt\Administrator\Trait\LimitEnforcementTrait;

class TierService
{
    use SubscriptionValidationTrait;
    use LimitEnforcementTrait;

    public function __construct()
    {
        $this->productSlug = 'com_yourext';   // must match your product_slug in Subscriptions Manager
        $this->initializeSubscriptionValidation();
        $this->registerResources();
    }

    protected function registerResources(): void
    {
        $db = \Joomla\CMS\Factory::getContainer()->get('DatabaseDriver');

        $this->registerResource('max_articles', [
            'feature_key'    => 'max_articles',
            'label'          => 'Articles',
            'resource_type'  => 'cumulative',
            'count_callback' => fn() => (int) $db->setQuery(
                'SELECT COUNT(*) FROM #__yourext_articles WHERE state = 1'
            )->loadResult(),
        ]);

        $this->registerResource('api_calls_monthly', [
            'feature_key'   => 'api_calls_monthly',
            'label'         => 'API Calls',
            'resource_type' => 'periodic',
        ]);

        $this->registerResource('custom_templates', [
            'feature_key'   => 'custom_templates',
            'label'         => 'Custom Templates',
            'resource_type' => 'boolean',
        ]);
    }
}

The trait needs to know where your Subscriptions Manager lives. Either hard-code your validation URL in the trait, or expose it as a component config option:

// In SubscriptionValidationTrait
protected function getValidationApiUrl(): string
{
    return 'https://www.yoursite.com/api/index.php/v1/subsmgr/validate';
}

Store the Subscription Key

Add a subscription_key field to your component's config.xml:

<fieldset name="subscription" label="COM_YOUREXT_CONFIG_SUBSCRIPTION">
    <field
        name="subscription_key"
        type="text"
        label="COM_YOUREXT_CONFIG_SUBSCRIPTION_KEY"
        description="COM_YOUREXT_CONFIG_SUBSCRIPTION_KEY_DESC"
        filter="string"
        size="60"
    />
</fieldset>

The trait reads this via ComponentHelper::getParams('com_yourext')->get('subscription_key').


Enforce Limits in Your Code

Before any action that should be gated, check the limit:

$tier = new TierService();
$check = $tier->checkResourceLimit('max_articles');

if (!$check['allowed']) {
    $app->enqueueMessage($check['message'], 'error');
    return false;
}

// proceed with the action...

The $check response contains everything you need to render a warning or block the action:

[
    'allowed'          => false,
    'message'          => 'Articles limit reached (100/100 used). Upgrade to Premium tier.',
    'current'          => 100,
    'limit'            => 100,
    'remaining'        => 0,
    'percentage'       => 100.0,
    'tier'             => 'standard',
    'warning_level'    => 'urgent',    // null | 'info' | 'warning' | 'urgent'
    'upgrade_required' => true,
    'resource_type'    => 'cumulative',
]
  • Boolean features return ['allowed' => true|false] based on the tier value.
  • Periodic features allow configurable grace (e.g. 110% of limit) and reset on the 1st of each month.
  • Cumulative features reject immediately when at the limit.

Display Usage in Your UI

Use a sidebar meter on form pages to show progressive disclosure. Colour-code using Bootstrap's text-bg-* classes for theme compatibility:

$check = $tier->checkResourceLimit('max_articles');
$class = match ($check['warning_level']) {
    'urgent'  => 'text-bg-danger',
    'warning' => 'text-bg-warning',
    'info'    => 'text-bg-info',
    default   => 'text-bg-success',
};
?>
<div class="card">
    <div class="card-body">
        <h5>Articles</h5>
        <div class="progress">
            <div class="progress-bar <?= $class ?>"
                 style="width: <?= min(100, $check['percentage']) ?>%">
                <?= $check['current'] ?> / <?= $check['limit'] === -1 ? '&infin;' : $check['limit'] ?>
            </div>
        </div>
        <?php if ($check['upgrade_required']): ?>
            <p class="text-danger small mt-2"><?= $check['message'] ?></p>
        <?php endif; ?>
    </div>
</div>
Example Feature comparison table
Example Feature comparison table

The Validation API

Your extension calls the validation endpoint on your Subscriptions Manager installation; the trait handles this for you.

Endpoint

POST https://www.yoursite.com/api/index.php/v1/subsmgr/validate

Request

{
    "subscription_key": "subsmgr-2026-a1b2c3d4e5f6a7b8",
    "product_slug": "com_yourext",
    "domain": "customer.com",
    "fingerprint": "sha256-of-joomla-secret"
}

Response (RSA-SHA256 signed)

{
    "valid": true,
    "tier": "premium",
    "features": {
        "max_articles": 1000,
        "api_calls_monthly": 100000,
        "custom_templates": true,
        "export_formats": 5
    },
    "expires_date": "2027-04-20T23:59:59Z",
    "subscribed_to": "Acme Corp",
    "is_trial": false,
    "validation_timestamp": "2026-04-20T10:30:00Z",
    "fingerprint_hash": "abc123...",
    "signature": "base64-rsa-signature"
}

Why the fingerprint matters

The fingerprint is a hash of the Joomla installation's secret. Your Subscriptions Manager binds the signed response to this fingerprint so a customer cannot copy a valid cached response from one site to another — the receiving site will not match its own fingerprint.

Caching

The trait caches validated responses for 15 minutes (paid keys) or 24 hours (trial keys). If your API is unreachable, it falls back to the last valid cached response; if that has expired too, the customer drops to trial limits automatically.


Testing Checklist

  • Install your extension on a clean Joomla site.
  • Enter a trial subscription key — check that trial limits apply.
  • Create resources up to the trial limit — verify the sidebar turns yellow then red.
  • Attempt to exceed the limit — verify the action is blocked.
  • Enter a paid key — verify limits increase.
  • Disconnect the network — verify the extension keeps working from cache.
  • Wait 15 minutes with no network — verify it falls through to trial tier gracefully.
  • Try copying a cached response from another site — verify the signature check rejects it.

Common Pitfalls

Cause: product_slug mismatch between your extension and the product registered in your Subscriptions Manager.

Fix: Check the spelling exactly matches the product_slug column on your product features table.

Cause: The count_callback is throwing an exception (often a missing table on fresh install).

Fix: Wrap the callback in try/catch, or check the table exists before querying.

Cause: The customer's cache still holds the old response.

Fix: Call $tier->invalidateCache() from your extension, or wait up to 15 minutes for the cache to expire.

Cause: The public key embedded in your extension does not match the private key on your Subscriptions Manager installation.

Fix: Re-export the public key from Subscriptions Manager and update it in your extension. If you rotate keys, you will need to ship a new release of your extension.

Get support

Contact us

Questions, bug reports, feature requests — get in touch.

  • Website: multizone.co.uk
  • Email: This email address is being protected from spambots. You need JavaScript enabled to view it.
  • In-app help: Each Multizone component has a checklist or explainer on its dashboard and help throughout the component.

What support covers

All Multizone extensions are freely downloadable as trials.

  • Trial — community support via the documentation and changelog.
  • Standard / Premium / Enterprise — direct email support; response priority scales with tier.
  • Extension Directory – host your extension on our Suscription Manager platform (Fees apply).
  • Custom development – extend Subscriptions Manager; build a customer-side extension with subscription tier support. Host your extension on our Suscription Manager platform (Ask for a quotation).

Documentation and changelogs for every extension can be found at multizone.co.uk/documentation.

Licences, trademarks, source code licences and attributions

928uk® is a trademark of Multizone Limited, registered in the UK. Multizone and this site is not affiliated with or endorsed by The Joomla! Project™. Any products and services provided through this site are not supported or warrantied by The Joomla! Project or Open Source Matters, Inc. Use of the Joomla!® name, symbol, logo and related trademarks is permitted under a limited licence granted by Open Source Matters, Inc. AdMob™, AdSense™, AdWords™, Android™, Chrome OS™, Chromebook™, Chrome™, DART™, Flutter™, Firebase™, Firestore™, Fuchsia™, Gmail™, Google Maps™, Google Pixel™, Google Play™, Pixelbook Go™, and Pixel™ and other trademarks listed at the Google Brand Resource center are trademarks of Google LLC and this site is not endorsed by or affiliated with Google in any way. Apple and the Apple logo are trademarks of Apple Inc., registered in the U.S. and other countries. App Store is a service mark of Apple Inc. The OSI logo trademark is the trademark of Open Source Initiative. Any other product or company names may be trademarks™ or registered® trademarks of their respective holders. Use of these trademarks in articles here does not apply affiliation or endorsement by any of them.

Where the source code is published here on multizone.co.uk or on our GitHub by Angus Fox, Multizone Limited it is licenced according to the open source practice for the project concerned.

BSD 3-Clause "New" or "Revised" Licence
Original source code for mobile apps are licenced using the same licence as the one used by "The Flutter Authors". This Licence, the BSD 3-Clause "New" or "Revised" Licence (bsd-3-clause) is a permissive licence with a clause that prohibits others from using the name of the project or its contributors to promote derived products without written consent.
GNU General Public Licence v3.0 or later
Original source code for Joomla! published here on multizone.co.uk by Angus Fox, Multizone Limited is licenced using the GNU General Public Licence. This Licence, the GNU General Public Licence Version 3 or later (gpl-3.0+) is the most widely used free software licence and has a strong copyleft requirement. When distributing derived works, the source code of the work must be made available under the same licence.

Please respect the licences and dont use the name of this site or our company to promote derived products without written consent. I mean, why would you? You're not us!

Amazon Associate
As an Amazon Associate we earn from qualifying purchases.
Logo
Our Logo Image is by Freepik. We chose it because its an M and also the letter A twice - and that represents us.
Graphics
Our images representing user experience and interface design are from Freepik here and here and here and here and here.