Multi-Tenant Setup Guide

Overview

Wagtail Subscriptions automatically detects and supports multi-tenant architectures using django-tenant-schemas or similar packages. In multi-tenant mode, subscriptions are associated with tenants rather than individual users.

Architecture

Single-Tenant Mode (Default)

User → Subscription → Plan → Features

Multi-Tenant Mode

Tenant → Subscription → Plan → Features
  ↓
Users (inherit tenant's subscription)

Setup with django-tenant-schemas

1. Install Dependencies

pip install django-tenant-schemas
pip install wagtail-subscriptions

2. Configure Django Settings

# settings.py

INSTALLED_APPS = [
    'tenant_schemas',  # Must be first
    'django.contrib.contenttypes',
    'django.contrib.auth',
    # ... other apps
    'wagtail_subscriptions',
]

# Database configuration
DATABASES = {
    'default': {
        'ENGINE': 'tenant_schemas.postgresql_backend',
        'NAME': 'your_database',
        'USER': 'your_user',
        'PASSWORD': 'your_password',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

# Tenant configuration
TENANT_MODEL = "customers.Client"  # Your tenant model
TENANT_DOMAIN_MODEL = "customers.Domain"

# Shared apps (available to all tenants)
SHARED_APPS = [
    'tenant_schemas',
    'django.contrib.contenttypes',
    'django.contrib.auth',
    'wagtail_subscriptions',  # Shared for plan management
]

# Tenant-specific apps
TENANT_APPS = [
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'wagtail.contrib.forms',
    'wagtail.contrib.redirects',
    'wagtail.embeds',
    'wagtail.sites',
    'wagtail.users',
    'wagtail.snippets',
    'wagtail.documents',
    'wagtail.images',
    'wagtail.search',
    'wagtail.admin',
    'wagtail',
    # Your tenant-specific apps
]

# Middleware
MIDDLEWARE = [
    'tenant_schemas.middleware.TenantMiddleware',
    'django.middleware.security.SecurityMiddleware',
    # ... other middleware
]

# Public schema name
PUBLIC_SCHEMA_NAME = 'public'

3. Create Tenant Model

# customers/models.py
from django.db import models
from tenant_schemas.models import TenantMixin

class Client(TenantMixin):
    """Tenant model with subscription support."""
    
    name = models.CharField(max_length=100)
    created_on = models.DateField(auto_now_add=True)
    
    # Subscription will be automatically linked
    # via wagtail_subscriptions.models.Subscription.tenant
    
    auto_create_schema = True
    auto_drop_schema = True
    
    def __str__(self):
        return self.name

class Domain(models.Model):
    """Domain model for tenant routing."""
    
    domain = models.CharField(max_length=253, unique=True, db_index=True)
    tenant = models.ForeignKey(Client, on_delete=models.CASCADE, related_name='domains')
    is_primary = models.BooleanField(default=True)
    
    def __str__(self):
        return self.domain

4. Run Migrations

# Create public schema and shared tables
python manage.py migrate_schemas --shared

# Create tenant schemas
python manage.py migrate_schemas

5. Create Subscription Plans (Public Schema)

# In Django shell or management command
from django.core.management import call_command
from tenant_schemas.utils import schema_context

# Switch to public schema
with schema_context('public'):
    from wagtail_subscriptions.models import SubscriptionPlan, Feature, Module
    
    # Create plans
    starter = SubscriptionPlan.objects.create(
        name="Starter",
        slug="starter",
        price=9.99,
        billing_period="monthly"
    )
    
    pro = SubscriptionPlan.objects.create(
        name="Professional",
        slug="pro",
        price=29.99,
        billing_period="monthly"
    )
    
    # Create features
    module = Module.objects.create(name="Core Features")
    
    users_feature = Feature.objects.create(
        name="User Accounts",
        slug="user_accounts",
        module=module,
        feature_type="quota",
        default_quota=10
    )
    
    # Assign features to plans
    starter.plan_features.create(feature=users_feature, quota=5)
    pro.plan_features.create(feature=users_feature, quota=50)

Usage in Multi-Tenant Mode

Creating Tenant with Subscription

from customers.models import Client, Domain
from wagtail_subscriptions.models import Subscription, SubscriptionPlan
from wagtail_subscriptions.payments import get_payment_processor

# Create tenant
tenant = Client.objects.create(
    schema_name='acme_corp',
    name='Acme Corporation'
)

# Create domain
Domain.objects.create(
    domain='acme.example.com',
    tenant=tenant,
    is_primary=True
)

# Get plan
plan = SubscriptionPlan.objects.get(slug='pro')

# Create subscription for tenant
processor = get_payment_processor('stripe')

# Create customer in payment processor
customer = processor.create_customer(
    email='billing@acme.com',
    name='Acme Corporation',
    metadata={'tenant_id': tenant.id}
)

# Create subscription
subscription = Subscription.objects.create(
    tenant=tenant,  # Link to tenant, not user
    plan=plan,
    status='active',
    stripe_customer_id=customer['id']
)

Checking Feature Access

# In views (automatic tenant detection)
from wagtail_subscriptions.permissions.decorators import feature_required

@feature_required('advanced_analytics')
def analytics_view(request):
    """
    Automatically checks current tenant's subscription.
    TenantMiddleware sets request.tenant.
    """
    return render(request, 'analytics.html')

Template Usage

{% load subscription_tags %}

<!-- Automatically uses current tenant's subscription -->
{% if request|has_feature:'api_access' %}
    <a href="/api/">API Access</a>
{% endif %}

<!-- Get tenant subscription info -->
{% subscription_info as sub_info %}
<div class="tenant-subscription">
    <h3>{{ sub_info.name }}</h3>
    <p>Plan: {{ sub_info.plan }}</p>
    <p>Type: {{ sub_info.type }}</p>  <!-- Will show "tenant" -->
</div>

Programmatic Access

from wagtail_subscriptions.permissions.tenant_manager import TenantSubscriptionManager

# Get tenant's subscription
manager = TenantSubscriptionManager()
subscription = manager.get_subscription(request)

# Check feature access
if subscription and subscription.has_feature_access('api_access'):
    # Grant access
    pass

# Get feature quota
quota = subscription.get_feature_quota('api_calls')

Tenant Provisioning Workflow

Complete Tenant Setup

# management/commands/create_tenant.py
from django.core.management.base import BaseCommand
from customers.models import Client, Domain
from wagtail_subscriptions.models import Subscription, SubscriptionPlan
from wagtail_subscriptions.payments import get_payment_processor

class Command(BaseCommand):
    help = 'Create new tenant with subscription'
    
    def add_arguments(self, parser):
        parser.add_argument('schema_name', type=str)
        parser.add_argument('domain', type=str)
        parser.add_argument('name', type=str)
        parser.add_argument('email', type=str)
        parser.add_argument('plan_slug', type=str)
    
    def handle(self, *args, **options):
        # Create tenant
        tenant = Client.objects.create(
            schema_name=options['schema_name'],
            name=options['name']
        )
        self.stdout.write(f"Created tenant: {tenant.name}")
        
        # Create domain
        domain = Domain.objects.create(
            domain=options['domain'],
            tenant=tenant,
            is_primary=True
        )
        self.stdout.write(f"Created domain: {domain.domain}")
        
        # Get plan
        plan = SubscriptionPlan.objects.get(slug=options['plan_slug'])
        
        # Create payment processor customer
        processor = get_payment_processor('stripe')
        customer = processor.create_customer(
            email=options['email'],
            name=options['name'],
            metadata={'tenant_id': tenant.id}
        )
        
        # Create subscription
        subscription = Subscription.objects.create(
            tenant=tenant,
            plan=plan,
            status='trialing' if plan.trial_period_days > 0 else 'active',
            stripe_customer_id=customer['id']
        )
        
        self.stdout.write(
            self.style.SUCCESS(
                f"Successfully created tenant with {plan.name} subscription"
            )
        )

Usage:

python manage.py create_tenant \
    acme_corp \
    acme.example.com \
    "Acme Corporation" \
    billing@acme.com \
    pro

Tenant-Aware Admin

Custom Admin Views

# admin/views.py
from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import render
from wagtail_subscriptions.models import Subscription

@staff_member_required
def tenant_subscriptions(request):
    """View all tenant subscriptions."""
    subscriptions = Subscription.objects.filter(
        tenant__isnull=False
    ).select_related('tenant', 'plan')
    
    return render(request, 'admin/tenant_subscriptions.html', {
        'subscriptions': subscriptions
    })

Wagtail Admin Integration

# wagtail_hooks.py
from wagtail import hooks
from wagtail.admin.menu import MenuItem
from django.urls import reverse

@hooks.register('register_admin_menu_item')
def register_tenant_menu():
    return MenuItem(
        'Tenant Subscriptions',
        reverse('admin:tenant_subscriptions'),
        icon_name='group',
        order=400
    )

Subscription Management

Upgrade/Downgrade Tenant Plan

from wagtail_subscriptions.models import Subscription
from wagtail_subscriptions.payments import get_payment_processor

def change_tenant_plan(tenant, new_plan):
    """Change tenant's subscription plan."""
    subscription = Subscription.objects.get(tenant=tenant, status='active')
    processor = get_payment_processor('stripe')
    
    # Update in payment processor
    processor.update_subscription(
        subscription_id=subscription.stripe_subscription_id,
        plan_id=new_plan.stripe_price_id,
        prorate=True
    )
    
    # Update local record
    subscription.plan = new_plan
    subscription.save()
    
    return subscription

Cancel Tenant Subscription

def cancel_tenant_subscription(tenant, immediate=False):
    """Cancel tenant's subscription."""
    subscription = Subscription.objects.get(tenant=tenant, status='active')
    
    # Cancel in payment processor
    subscription.cancel(immediate=immediate)
    
    if immediate:
        # Optionally disable tenant
        tenant.is_active = False
        tenant.save()

Billing Portal

Tenant Billing View

# views.py
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from wagtail_subscriptions.models import Subscription, Invoice

@login_required
def tenant_billing(request):
    """Tenant billing portal."""
    tenant = request.tenant
    subscription = Subscription.objects.filter(
        tenant=tenant,
        status__in=['active', 'trialing']
    ).first()
    
    invoices = Invoice.objects.filter(
        subscription=subscription
    ).order_by('-created_at')
    
    return render(request, 'billing/portal.html', {
        'subscription': subscription,
        'invoices': invoices,
        'tenant': tenant
    })

Webhooks in Multi-Tenant

Handling Tenant Webhooks

# views/webhooks.py
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse
from wagtail_subscriptions.payments import get_payment_processor
from wagtail_subscriptions.models import Subscription

@csrf_exempt
def stripe_webhook(request):
    """Handle Stripe webhooks for tenant subscriptions."""
    processor = get_payment_processor('stripe')
    
    try:
        event = processor.verify_webhook(
            payload=request.body,
            signature=request.META.get('HTTP_STRIPE_SIGNATURE'),
            secret=settings.STRIPE_WEBHOOK_SECRET
        )
        
        if event['type'] == 'customer.subscription.updated':
            # Find subscription by stripe_subscription_id
            subscription = Subscription.objects.get(
                stripe_subscription_id=event['data']['object']['id']
            )
            
            # Update subscription status
            subscription.status = event['data']['object']['status']
            subscription.save()
            
            # If subscription is canceled, optionally disable tenant
            if subscription.status == 'canceled':
                tenant = subscription.tenant
                tenant.is_active = False
                tenant.save()
        
        return JsonResponse({'status': 'success'})
        
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=400)

Testing Multi-Tenant Setup

Test Case Example

# tests/test_multi_tenant.py
from django.test import TestCase
from tenant_schemas.test.cases import TenantTestCase
from customers.models import Client
from wagtail_subscriptions.models import Subscription, SubscriptionPlan

class MultiTenantSubscriptionTest(TenantTestCase):
    def setUp(self):
        super().setUp()
        self.plan = SubscriptionPlan.objects.create(
            name="Test Plan",
            slug="test",
            price=9.99
        )
    
    def test_tenant_subscription(self):
        """Test tenant subscription creation."""
        subscription = Subscription.objects.create(
            tenant=self.tenant,
            plan=self.plan,
            status='active'
        )
        
        self.assertEqual(subscription.tenant, self.tenant)
        self.assertTrue(subscription.is_active)
    
    def test_feature_access(self):
        """Test tenant feature access."""
        subscription = Subscription.objects.create(
            tenant=self.tenant,
            plan=self.plan,
            status='active'
        )
        
        # Test feature access
        has_access = subscription.has_feature_access('test_feature')
        self.assertTrue(has_access)

Best Practices

1. Tenant Isolation

  • Always use schema_context when accessing tenant data

  • Never mix public and tenant schemas in queries

  • Use middleware for automatic tenant detection

2. Subscription Management

  • Link subscriptions to tenants, not users

  • Handle subscription cancellation gracefully

  • Implement grace periods for payment failures

3. Feature Access

  • Cache feature checks per request

  • Use middleware for automatic tenant detection

  • Implement quota tracking per tenant

4. Billing

  • Store billing contact separately from tenant admin

  • Send invoices to billing contact

  • Implement payment retry logic

5. Monitoring

  • Track subscription status per tenant

  • Monitor feature usage per tenant

  • Alert on payment failures

  • Log all subscription changes

Troubleshooting

Common Issues

Issue: Features not accessible after subscription creation

# Solution: Clear cache
from django.core.cache import cache
cache.clear()

Issue: Tenant not detected in views

# Solution: Ensure TenantMiddleware is properly configured
MIDDLEWARE = [
    'tenant_schemas.middleware.TenantMiddleware',  # Must be first
    # ... other middleware
]

Issue: Subscription queries returning wrong tenant

# Solution: Always filter by tenant
subscription = Subscription.objects.filter(
    tenant=request.tenant,
    status='active'
).first()