🔒 Feature Gating¶
Note
Feature gating requires stripe_mode: advanced — the decorators, mixins, and
usage utilities are built on the dj-stripe models (PlanConfiguration,
UsageRecord) that only generate in advanced mode.
Control access to features based on subscription plans and usage limits.
Overview¶
Feature gating allows you to:
- Restrict features by subscription tier (Free, Pro, Enterprise)
- Enforce usage limits (API calls, storage, users per team)
- Monetize features with clear upgrade paths
- Prevent feature creep in lower tiers
- Encourage upgrades with locked premium features
Django Keel provides decorators, mixins, and utilities for subscription-based access control.
Quick Start¶
Enable Feature Gating¶
Generate your project with use_stripe: true and stripe_mode: advanced. Feature gating lives in apps/billing/decorators.py (decorators and mixins) and apps/billing/utils.py (helper functions).
Basic Usage¶
from apps.billing.decorators import subscription_required, feature_required
@subscription_required()
@feature_required("advanced_analytics")
def analytics_view(request):
"""Only accessible to users with active subscription and feature."""
return render(request, "analytics.html")
All decorators are factories - always call them with parentheses, even without arguments.
Decorators¶
@subscription_required()¶
Ensures the user has an active subscription:
from apps.billing.decorators import subscription_required
@subscription_required()
def premium_feature(request):
"""Requires any active subscription."""
return render(request, "premium.html")
@subscription_required(redirect_url="/pricing/")
def another_view(request): ...
Parameters: redirect_url (default /billing/subscribe/), message, ajax_response (return a JSON 403 instead of redirecting).
Behavior:
- ✅ User with active (or trialing) subscription → Access granted
- ❌ Not authenticated → Redirect to login (or JSON 401 with
ajax_response=True) - ❌ No active subscription → Warning message and redirect (or JSON 403)
@feature_required(feature_key)¶
Checks if the subscription's plan includes a specific feature:
from apps.billing.decorators import feature_required
@feature_required("api_access")
def api_dashboard(request):
"""Requires subscription with API access feature."""
return render(request, "api_dashboard.html")
@feature_required("advanced_analytics", redirect_url="/pricing/")
def analytics_view(request): ...
Parameters: feature_key, redirect_url (default /billing/upgrade/), message, ajax_response.
@plan_required(*plan_slugs)¶
Requires one of the given plan slugs. Pass slugs as positional arguments, not a list:
from apps.billing.decorators import plan_required
@plan_required("pro")
def pro_only_feature(request):
"""Only for Pro tier subscribers."""
return render(request, "pro_feature.html")
@plan_required("pro", "enterprise")
def premium_feature(request):
"""For Pro or Enterprise subscribers."""
return render(request, "premium.html")
Plan slugs are matched against PlanConfiguration rows (see Plan Configuration below).
@usage_limit_check(metric)¶
Blocks the view when the user is already over the limit for a metric:
from apps.billing.decorators import usage_limit_check
@usage_limit_check("api_calls")
def api_endpoint(request):
"""Blocked with a 429/redirect once the user is over their limit."""
return JsonResponse({"data": "response"})
Parameters: metric, redirect_url (default /billing/upgrade/), message, ajax_response (returns JSON with status 429 when over limit).
Limits come from the subscription's SubscriptionMetadata.usage_limits JSON. The decorator only checks the limit - it does not record usage. Call record_usage() yourself (see Usage Tracking).
Class-Based Views¶
The mixins live in apps/billing/decorators.py alongside the decorators.
SubscriptionRequiredMixin¶
from apps.billing.decorators import SubscriptionRequiredMixin
from django.views.generic import TemplateView
class PremiumDashboard(SubscriptionRequiredMixin, TemplateView):
template_name = "dashboard.html"
# Optional overrides:
# subscription_redirect_url = "/billing/subscribe/"
# subscription_message = "..."
FeatureRequiredMixin¶
from apps.billing.decorators import FeatureRequiredMixin
class AnalyticsView(FeatureRequiredMixin, TemplateView):
template_name = "analytics.html"
required_feature = "advanced_analytics"
# Optional: feature_redirect_url, feature_message
PlanRequiredMixin¶
from apps.billing.decorators import PlanRequiredMixin
class EnterpriseView(PlanRequiredMixin, TemplateView):
template_name = "enterprise.html"
required_plans = ["enterprise"]
# Optional: plan_redirect_url, plan_message
There is no usage-limit mixin; use the usage_limit_check() decorator (e.g. via method_decorator) for class-based views.
Programmatic Checks¶
All helpers are in apps/billing/utils.py.
Check Subscription¶
from apps.billing.utils import has_active_subscription, get_active_subscription
if has_active_subscription(request.user):
# User can access premium features
...
subscription = get_active_subscription(request.user) # dj-stripe Subscription or None
Check Feature¶
from apps.billing.utils import check_feature_access
if check_feature_access(request.user, "api_access"):
# User has API access feature
api_key = generate_api_key(request.user)
Check Usage Limit¶
from apps.billing.utils import check_usage_limit
# Returns True when the user is OVER the limit (or has no subscription)
if check_usage_limit(request.user, "api_calls"):
return JsonResponse({"error": "Usage limit exceeded"}, status=429)
result = make_api_call()
Note the semantics: check_usage_limit(user, metric) returns True when the user is over the limit, and also when they have no active subscription.
Get Plan Features¶
from apps.billing.utils import get_active_subscription, get_subscription_features
subscription = get_active_subscription(request.user)
features = get_subscription_features(subscription) # dict from PlanConfiguration.features
API (DRF) Integration¶
Subscription Required¶
from rest_framework.decorators import api_view
from apps.billing.decorators import subscription_required
@api_view(["GET"])
@subscription_required(ajax_response=True)
def premium_api(request):
"""API endpoint requires active subscription."""
return Response({"data": "premium data"})
Feature-Based Permissions¶
from rest_framework.permissions import BasePermission
from apps.billing.utils import check_feature_access
class HasAPIAccess(BasePermission):
"""Custom permission for API access feature."""
def has_permission(self, request, view):
return check_feature_access(request.user, "api_access")
class MyAPIView(APIView):
permission_classes = [HasAPIAccess]
def get(self, request):
return Response({"data": "api response"})
Plan Configuration¶
Plans are database rows, not settings. The PlanConfiguration model (apps/billing/models.py) links a Stripe product to a plan slug, features, and limits:
from apps.billing.models import PlanConfiguration
from djstripe.models import Product, Price
product = Product.objects.get(name="Pro")
PlanConfiguration.objects.create(
stripe_product=product,
stripe_price=Price.objects.get(product=product, active=True),
name="Pro Plan",
slug="pro",
features={
"advanced_analytics": True,
"custom_domains": True,
"priority_support": True,
},
limits={
"api_calls": 10000,
"storage_gb": 50,
"team_members": 10,
},
)
Create one PlanConfiguration per Stripe product - via the Django admin, a data migration, or a setup script. @plan_required() matches against slug, and check_feature_access() / get_subscription_features() read features.
Per-subscription overrides live in SubscriptionMetadata (features, usage_limits, current_usage JSON fields), which check_feature_access() and check_usage_limit() consult first when present.
Usage Tracking¶
Recording Usage¶
Usage is not recorded automatically - record it where the work happens:
from apps.billing.utils import get_active_subscription, record_usage
subscription = get_active_subscription(request.user)
record_usage(subscription, metric="api_calls", quantity=1)
record_usage(subscription, metric, quantity) creates a UsageRecord row.
Usage Model¶
UsageRecord (apps/billing/models.py) has five fields: subscription (FK to dj-stripe Subscription), metric, quantity, an auto-set timestamp, and a metadata JSONField for arbitrary extra context.
from apps.billing.models import UsageRecord
# All API call records for a subscription
records = UsageRecord.objects.filter(
subscription=subscription,
metric="api_calls",
)
Best Practices¶
- Clear upgrade paths - Always show what plan includes the feature
- Graceful degradation - Disable features, don't break the app
- Usage warnings - Alert users when approaching limits (80%, 90%)
- Feature discoverability - Show locked features with upgrade CTA
- Track metrics - Monitor which features drive upgrades
- Testing - Test all subscription tiers
- Documentation - Document all gated features clearly
Common Patterns¶
Tiered Feature Access¶
from apps.billing.utils import get_active_subscription, get_subscription_features
def can_create_project(user):
"""Check if user can create more projects."""
subscription = get_active_subscription(user)
if not subscription:
return user.projects.count() < 3 # free tier
features = get_subscription_features(subscription)
limit = features.get("projects")
if limit is None: # Unlimited
return True
return user.projects.count() < limit
Feature Flags + Gating¶
Combine feature flags with gating:
from waffle import flag_is_active
from apps.billing.utils import check_feature_access
def advanced_feature_view(request):
# Check feature flag (gradual rollout)
if not flag_is_active(request, "new_analytics"):
return redirect("dashboard")
# Check subscription (monetization)
if not check_feature_access(request.user, "advanced_analytics"):
return render(request, "upgrade_required.html")
# Both checks passed
return render(request, "analytics.html")
Troubleshooting¶
User Can't Access Feature¶
Check subscription status:
from apps.billing.utils import get_active_subscription
subscription = get_active_subscription(user)
print(subscription) # None means no active/trialing subscription
Check plan configuration:
from apps.billing.models import PlanConfiguration
plan = PlanConfiguration.objects.get(slug="pro")
print(plan.features)
print(plan.limits)
Usage Not Tracking¶
Make sure your code calls record_usage() - nothing records usage automatically:
from apps.billing.models import UsageRecord
records = UsageRecord.objects.filter(subscription=subscription, metric="api_calls")
for r in records:
print(f"{r.timestamp}: {r.quantity}")
Further Reading¶
- Stripe Integration - Set up subscription billing
- Teams & Organizations - Team-based subscriptions
- Feature Flags - Gradual feature rollouts
Related¶
- Basic Stripe Mode: Simple checkout without feature gating
- Advanced Stripe Mode: Full subscription management with feature gating
- Teams: Per-seat billing with team-based limits