Subscription Tier System Documentation
Overview
The subscription tier system manages user subscriptions with feature-based access control. Users get assigned to tiers by purchasing products, and each tier grants access to specific features and limits.
How It Works
1. Creating Subscription Tiers (Admin)
Navigate: Products → Subscription Tiers → Create New Tier
Configure the tier:
- Tier Name: Internal identifier (e.g., "basic", "premium", "pro")
- Display Name: What users see (e.g., "Basic Plan", "Premium Member")
- Tier Level: Numeric hierarchy - higher numbers = better tiers (e.g., 10, 20, 30)
- Description: Rich text shown to users describing the tier
- Features: Configure limits and access for this tier (see below)
- Active: Toggle to enable/disable the tier
Tier Name: premium
Display Name: Premium Plan
Tier Level: 20
Description: Full access with up to 10 devices
Features:
- Maximum Devices: 10
- Custom Rules: Enabled
- Advanced Filters: Enabled2. Configuring Tier Features
Features are automatically discovered from JSON definition files:
Plugin Features: Each plugin defines its features in /plugins/{plugin}/tier_features.json
Example: /plugins/controld/tier_features.json
{
"max_devices": {
"type": "integer",
"label": "Maximum Devices",
"description": "Maximum number of devices allowed for this tier",
"default": 1,
"min": 0,
"max": 999
}
}When you edit a tier, the admin UI automatically shows all available features from all plugins. Simply set the values for each tier.
Note: Plugin features are automatically prefixed with the plugin name (e.g., max_devices becomes controld_max_devices).
3. Linking Tiers to Products
Navigate: Products → Edit Product
Assign the tier:
- Select a subscription tier from the dropdown
- Save the product
4. User Assignment
Automatic Assignment (Purchase):
- User purchases a product with an assigned tier
- User is removed from their current tier (if any)
- User is assigned to the new tier
- If the new tier is lower level than current, assignment is skipped (upgrade-only)
- Navigate to Subscription Tiers → View Members
- Add/remove users manually
- Useful for promotions, migrations, or support cases
5. Viewing Tier Members
Navigate: Products → Subscription Tiers → View Members
See:
- All users in the tier
- When they were assigned
- How they got the tier (purchase, manual, etc.)
- Current subscription status
6. Admin Settings
Six settings control subscription management behaviors (Products → Settings):
- subscription_downgrades_enabled - Allow users to downgrade to lower tiers
- subscription_downgrade_timing - When downgrades take effect (
immediateorend_of_period) - subscription_cancellation_enabled - Allow users to cancel subscriptions
- subscription_cancellation_timing - When cancellations take effect (
immediateorend_of_period) - subscription_reactivation_enabled - Allow users to reactivate cancelled subscriptions
- subscription_cancellation_prorate - Issue refunds for immediate cancellations
User Experience
Users manage their subscriptions from a dedicated subscription management page:
View Current Tier:
- See their current subscription tier and features
- View billing information and renewal date
- Always available
- See higher tiers and their features
- Purchase immediately with prorated billing
- Only if
subscription_downgrades_enabled = true - Takes effect based on
subscription_downgrade_timingsetting
- Only if
subscription_cancellation_enabled = true - Timing and refunds controlled by settings
- Only if
subscription_reactivation_enabled = true - Available for cancelled subscriptions before they expire
Developer Reference
Key Files
Models:
/data/subscription_tiers_class.php-SubscriptionTierandMultiSubscriptionTier
/adm/admin_subscription_tiers.php- List tiers/adm/admin_subscription_tier_edit.php- Create/edit tier/adm/admin_subscription_tier_members.php- View members
/views/change-subscription.php- Subscription management UI/logic/change_subscription_logic.php- Business logic
/includes/core_tier_features.json- Core features/plugins/{plugin}/tier_features.json- Plugin features
Database Structure
Main table: sbt_subscription_tiers
- Links to groups system via
sbt_grp_group_id - Features stored as JSONB in
sbt_features - Integrates with products via
pro_sbt_subscription_tier_id
Common Code Patterns
Check user's feature value:
// Get max devices for user (default to 1 if no tier)
$max_devices = SubscriptionTier::getUserFeature($user_id, 'controld_max_devices', 1);
// Check boolean feature
$has_premium = SubscriptionTier::getUserFeature($user_id, 'controld_advanced_filters', false);
// Use in logic
if ($device_count >= $max_devices) {
return "You've reached your device limit. Upgrade to add more.";
}Check minimum tier level:
// Require tier level 20 or higher
if (!SubscriptionTier::UserHasMinimumTier($user_id, 20)) {
header('Location: /upgrade-required');
exit;
}
// Or use helper that redirects automatically:
SubscriptionTier::requireMinimumTier($user_id, 20, '/change-subscription');Get user's tier:
$tier = SubscriptionTier::GetUserTier($user_id);
if ($tier) {
echo "Your plan: " . $tier->get('sbt_display_name');
echo "Tier level: " . $tier->get('sbt_tier_level');
}Get available upgrades:
$upgrades = SubscriptionTier::getUpgradeOptions($user_id);
foreach ($upgrades as $option) {
$tier = $option['tier'];
$products = $option['products'];
// Display upgrade cards
}Creating Plugin Features
Create /plugins/{yourplugin}/tier_features.json:
{
"your_feature_name": {
"type": "integer",
"label": "Feature Display Name",
"description": "Help text for admins",
"default": 1,
"min": 0,
"max": 100
},
"another_feature": {
"type": "boolean",
"label": "Enable Premium Feature",
"description": "Allow access to premium features",
"default": false
}
}Feature types:
integer- Numeric limits (shows number input with optional min/max)boolean- True/false flags (shows checkbox)string- Text values (shows text input)
Rolling Out a New Feature to Existing Tiers
Adding a tier-gated feature is three steps. Skipping any of them silently breaks the feature for the wrong audience.
- Schema — declare the key in
tier_features.json(or coresettings.json-equivalent). - Code — gate the UI/logic via
$tier->getFeature('feature_key', $default)orSubscriptionTier::getUserFeature(...). - Per-tier values — explicitly set the flag on every existing tier row in
sbt_subscription_tiers.sbt_featuresvia the admin Subscription Tiers UI (/admin/admin_subscription_tier_edit?id=N).
default in the schema is not a per-tier default — it's a fallback used when a tier row has no entry for the key. So if you ship a boolean feature with default: false and forget step 3, every paying tier silently gets false until an admin manually toggles it on. The feature looks broken from the user's side, but the only "bug" is missing per-tier values.For brand-new tiers, the same rule applies — when creating a tier, set every relevant feature explicitly rather than relying on schema defaults.
Important: MultiGroup Filter Keys
When querying for groups, use the correct option keys:
// ❌ WRONG - Uses column names directly
$groups = new MultiGroup([
'grp_name' => 'Basic Plan',
'grp_category' => 'subscription_tier'
]);
// ✅ CORRECT - Uses MultiGroup option keys
$groups = new MultiGroup([
'group_name' => 'Basic Plan',
'category' => 'subscription_tier'
]);MultiGroup option keys:
group_name→grp_namecategory→grp_categorygroup_id→grp_group_iduser_id→grp_usr_user_iddeleted→grp_delete_time(bool)
/data/[table]_class.php to see which option keys each Multi class accepts.Automatic Integration
The system automatically:
- Creates a group for each tier in
grp_groupswithgrp_category = 'subscription_tier' - Assigns users to tier groups when they purchase products
- Removes users from old tiers when upgrading
- Tracks all changes in the
change_trackingtable - Prevents downgrades via purchase (upgrade-only)
Troubleshooting
"Tier already exists" error when creating new tier:
- Check you're using correct MultiGroup option keys (see above)
- Verify no existing tier has that name
- Check JSON file exists and has valid syntax
- Ensure file is readable by web server
- Verify product has
pro_sbt_subscription_tier_idset - Check error logs for assignment failures
- Always provide default value in
getUserFeature() - Verify feature key matches JSON definition
- Check user has a tier assigned
false) for paying customers:
- The per-tier value was never set on
sbt_subscription_tiers.sbt_featuresfor that tier — the schemadefaultis filling in. - Open
/admin/admin_subscription_tier_edit?id=Nfor each affected tier and toggle the feature on. See "Rolling Out a New Feature to Existing Tiers" above.
Tier Gating (Content Access Control)
Tier gating restricts access to any entity based on the viewer's subscription tier. When a user lacks the required tier, they see a prompt to subscribe or upgrade.
How It Works
Each gatable entity has a {prefix}_tier_min_level field. When set to a tier level value (e.g., 10, 20, 30), only users at that tier or higher can see the full content. Admins (permission 5+) always bypass the gate.
Making an Entity Tier-Gatable
- Add the field to
$field_specifications:
'{prefix}_tier_min_level' => array('type'=>'int4', 'is_nullable'=>true),- For early access support (optional), also add:
'{prefix}_tier_public_after_hours' => array('type'=>'int4', 'is_nullable'=>true),- Call
authenticate_tier()in the view:
$session = SessionControl::get_instance();
$access = $entity->authenticate_tier($session);
if ($access['allowed']) {
// Render full content
} else {
require_once(PathHelper::getIncludePath('includes/tier_gate_prompt.php'));
render_tier_gate_prompt($access);
}- For entities with
authenticate_read()(files, videos), add inside that method:
if ($this->get('{prefix}_tier_min_level')) {
$tier_access = $this->authenticate_tier($session);
if (!$tier_access['allowed']) return false;
}- Add a tier dropdown to the admin edit page:
require_once(PathHelper::getIncludePath('data/subscription_tiers_class.php'));
$tier_options = ['' => 'No tier required'];
$all_tiers = MultiSubscriptionTier::GetAllActive();
foreach ($all_tiers as $tier) {
$tier_options[$tier->get('sbt_tier_level')] = $tier->get('sbt_display_name') . ' (Level ' . $tier->get('sbt_tier_level') . ')';
}
$formwriter->dropinput('{prefix}_tier_min_level', 'Minimum Tier Required', [
'options' => $tier_options
]);Gate Prompt Component
includes/tier_gate_prompt.php provides two functions:
render_tier_gate_prompt($access, $options)— Renders the paywall prompt. Pass['preview_html' => $html]in options to show a preview before the gate.get_tier_gate_preview_html($body_text)— Returns truncated preview HTML based on thetier_gate_preview_lengthsetting.
authenticate_tier() Return Value
Returns an array with:
allowed(bool) — Whether access is grantedreason(string|null) —'not_logged_in'or'tier_too_low'required_level(int|null) — The tier level neededuser_level(int|null) — The user's current tier levelrequired_tier(SubscriptionTier|null) — The required tier objectupgrade_options(array) — Available upgrade products
Multi-Class Filtering
Use max_visible_tier_level in Multi class queries to filter by user's tier:
$user_tier_level = 0;
$tier = SubscriptionTier::GetUserTier($session->get_user_id());
if ($tier) $user_tier_level = $tier->get('sbt_tier_level');
$posts = new MultiPost(['published' => true, 'deleted' => false, 'max_visible_tier_level' => $user_tier_level]);Supported on: MultiPost, MultiEvent, MultiProduct.
Interaction with Other Access Controls
Tier gating is additive. The precedence is: soft delete > published state > visibility > permission level > group membership > tier requirement. Tier is always the last check.
Site Settings
tier_gate_preview_length— Characters of body text to show before the paywall (0 = no preview)tier_gate_hide_from_listings— Hide gated content from listings for users who lack the tier (RSS feeds always hide gated items)
Plugin Developer Usage
Plugin developers can use tier gating in plugin views by calling authenticate_tier() on any entity that has the {prefix}_tier_min_level field. The gate prompt component renders correctly in any theme context.
Related Documentation
/specs/subscription_tiers.md- Full specification and implementation detailsCLAUDE.md- System architecture and patterns