# 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
```bash
pip install django-tenant-schemas
pip install wagtail-subscriptions
```
### 2. Configure Django Settings
```python
# 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
```python
# 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
```bash
# 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)
```python
# 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
```python
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
```python
# 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
```django
{% load subscription_tags %}
{% if request|has_feature:'api_access' %}
API Access
{% endif %}
{% subscription_info as sub_info %}
{{ sub_info.name }}
Plan: {{ sub_info.plan }}
Type: {{ sub_info.type }}
```
### Programmatic Access
```python
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
```python
# 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:
```bash
python manage.py create_tenant \
acme_corp \
acme.example.com \
"Acme Corporation" \
billing@acme.com \
pro
```
## Tenant-Aware Admin
### Custom Admin Views
```python
# 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
```python
# 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
```python
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
```python
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
```python
# 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
```python
# 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
```python
# 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
```python
# Solution: Clear cache
from django.core.cache import cache
cache.clear()
```
**Issue**: Tenant not detected in views
```python
# Solution: Ensure TenantMiddleware is properly configured
MIDDLEWARE = [
'tenant_schemas.middleware.TenantMiddleware', # Must be first
# ... other middleware
]
```
**Issue**: Subscription queries returning wrong tenant
```python
# Solution: Always filter by tenant
subscription = Subscription.objects.filter(
tenant=request.tenant,
status='active'
).first()
```