C4 Code Diagram: TenantModel Hierarchy
Purpose: Class-level detail of the Django multi-tenant model architecture, showing inheritance hierarchy, field definitions, and relationships between tenant-scoped models.
Scope: Model layer - Django ORM models with multi-tenant architecture
Related Diagrams:
- C3-01: Django Backend Components - Component-level view
- SUP-01: Entity-Relationship Diagram - Database schema
- ADR-007: Django Multi-Tenant Architecture - Complete specification
Mermaid Class Diagram
Class Details
BaseTenantModel (django-multitenant library)
Package: django_multitenant.models
Purpose: Abstract base class from django-multitenant library providing automatic tenant filtering at the ORM level.
Implementation:
# From django-multitenant library (external)
from django.db import models
class TenantModel(models.Model):
"""
Abstract model for tenant-scoped models.
Automatically filters all queries by tenant_id.
"""
class Meta:
abstract = True
@classmethod
def get_queryset(cls):
"""Override queryset to filter by current tenant."""
# Implementation in django-multitenant library
pass
Key Features:
- Automatic query filtering by tenant_id
- Integration with PostgreSQL Row-Level Security (RLS)
- Thread-safe tenant context management
TenantModel (Custom Abstract Base)
Module: apps.tenants.models
Purpose: Custom abstract base class extending django-multitenant with additional validation and automatic tenant assignment.
Implementation:
from django.db import models
from django_multitenant.models import TenantModel as BaseTenantModel
class TenantModel(BaseTenantModel):
"""
Base class for all tenant-scoped models in CODITECT.
Extends django-multitenant with:
- Automatic tenant FK enforcement
- Validation (requires tenant context)
- Related name pattern (tenant.users, tenant.projects)
"""
tenant = models.ForeignKey(
'tenants.Tenant',
on_delete=models.CASCADE,
related_name='%(class)ss', # Dynamic: tenant.users, tenant.licenses
db_index=True,
help_text="Organization this record belongs to"
)
class Meta:
abstract = True
def save(self, *args, **kwargs):
"""
Save with automatic tenant assignment from context.
Raises:
ValueError: If no tenant context is set and tenant_id is None
"""
if not self.tenant_id:
from .context import get_current_tenant
current_tenant = get_current_tenant()
if current_tenant:
self.tenant_id = current_tenant.id
else:
raise ValueError(
f"Cannot save {self.__class__.__name__} without tenant context. "
"Set tenant explicitly or use TenantMiddleware."
)
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
"""
Delete with tenant context validation.
Ensures delete operations happen within correct tenant context.
"""
from .context import get_current_tenant
current_tenant = get_current_tenant()
if current_tenant and self.tenant_id != current_tenant.id:
raise PermissionError(
f"Cannot delete {self.__class__.__name__} from different tenant"
)
super().delete(*args, **kwargs)
Key Features:
- Automatic tenant FK: All child models inherit
tenantforeign key - Context validation: Prevents accidental cross-tenant operations
- Dynamic related names:
tenant.users,tenant.projects,tenant.license_sessions - Save protection: Requires tenant context or explicit tenant assignment
Tenant Model
Module: apps.tenants.models
Purpose: Core tenant (organization/company) model. NOT tenant-scoped (no self-reference).
Implementation:
from django.db import models
from django.utils.text import slugify
import uuid
class TenantStatus(models.TextChoices):
ACTIVE = 'active', 'Active'
SUSPENDED = 'suspended', 'Suspended'
TRIAL = 'trial', 'Trial'
CANCELLED = 'cancelled', 'Cancelled'
class PlanTier(models.TextChoices):
FREE = 'free', 'Free'
PRO = 'pro', 'Pro'
TEAM = 'team', 'Team'
ENTERPRISE = 'enterprise', 'Enterprise'
class Tenant(models.Model):
"""
Organization/company in the multi-tenant system.
Each tenant represents a separate customer organization with
complete data isolation.
"""
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False
)
# Basic info
name = models.CharField(
max_length=255,
unique=True,
help_text="Organization name (must be unique globally)"
)
slug = models.SlugField(
max_length=100,
unique=True,
help_text="URL-safe identifier (auto-generated from name)"
)
status = models.CharField(
max_length=20,
choices=TenantStatus.choices,
default=TenantStatus.ACTIVE
)
# Subscription
plan_tier = models.CharField(
max_length=50,
choices=PlanTier.choices,
default=PlanTier.FREE
)
max_users = models.IntegerField(
default=5,
help_text="Maximum concurrent license sessions"
)
max_projects = models.IntegerField(
default=3,
help_text="Maximum projects allowed"
)
# Billing (Stripe integration)
stripe_customer_id = models.CharField(
max_length=255,
null=True,
blank=True,
unique=True
)
stripe_subscription_id = models.CharField(
max_length=255,
null=True,
blank=True
)
# Security
encryption_key_id = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="Cloud KMS key ID for tenant-specific encryption"
)
# Metadata
settings = models.JSONField(
default=dict,
help_text="Tenant-specific settings (JSON)"
)
metadata = models.JSONField(
default=dict,
help_text="Additional metadata (JSON)"
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'tenants'
ordering = ['name']
indexes = [
models.Index(fields=['slug']),
models.Index(fields=['status']),
models.Index(fields=['stripe_customer_id']),
]
def __str__(self):
return self.name
def save(self, *args, **kwargs):
"""Auto-generate slug from name if not provided."""
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
@property
def is_active(self):
"""Check if tenant is in active status."""
return self.status == TenantStatus.ACTIVE
def get_user_count(self):
"""Get current active user count."""
return self.users.filter(is_active=True).count()
def get_project_count(self):
"""Get current active project count."""
return self.projects.filter(status='active').count()
def can_add_user(self):
"""Check if tenant can add more users."""
return self.get_user_count() < self.max_users
def can_add_project(self):
"""Check if tenant can add more projects."""
return self.get_project_count() < self.max_projects
Database Schema:
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL UNIQUE,
slug VARCHAR(100) NOT NULL UNIQUE,
status VARCHAR(20) NOT NULL DEFAULT 'active',
plan_tier VARCHAR(50) NOT NULL DEFAULT 'free',
max_users INTEGER NOT NULL DEFAULT 5,
max_projects INTEGER NOT NULL DEFAULT 3,
stripe_customer_id VARCHAR(255) UNIQUE,
stripe_subscription_id VARCHAR(255),
encryption_key_id VARCHAR(255),
settings JSONB NOT NULL DEFAULT '{}',
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_tenants_slug ON tenants(slug);
CREATE INDEX idx_tenants_status ON tenants(status);
CREATE INDEX idx_tenants_stripe ON tenants(stripe_customer_id) WHERE stripe_customer_id IS NOT NULL;
User Model
Module: apps.users.models
Purpose: User model with multi-tenant scoping and role-based access control.
Inheritance:
TenantModel- Multi-tenant scopingAbstractBaseUser- Django authenticationPermissionsMixin- Django permissions
Implementation:
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.db import models
from apps.tenants.models import TenantModel
import uuid
class UserRole(models.TextChoices):
OWNER = 'owner', 'Owner' # Full access (creates tenant)
ADMIN = 'admin', 'Admin' # User + project management
MEMBER = 'member', 'Member' # Project access, own data
VIEWER = 'viewer', 'Viewer' # Read-only access
class User(TenantModel, AbstractBaseUser, PermissionsMixin):
"""
Custom user model with tenant scoping.
Users belong to a single tenant and have a role within that tenant.
Email/username are unique per tenant, not globally.
"""
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False
)
# Authentication fields
email = models.EmailField(
max_length=255,
help_text="Email address (unique per tenant)"
)
username = models.CharField(
max_length=150,
help_text="Username (unique per tenant)"
)
password = models.CharField(max_length=255)
# Profile
full_name = models.CharField(
max_length=255,
blank=True,
help_text="User's full display name"
)
# Status
is_active = models.BooleanField(
default=True,
help_text="Can user login and use system?"
)
is_staff = models.BooleanField(
default=False,
help_text="Can user access Django admin?"
)
# Tenant role
role = models.CharField(
max_length=50,
choices=UserRole.choices,
default=UserRole.MEMBER,
help_text="User's role within their tenant"
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
last_login = models.DateTimeField(null=True, blank=True)
# Django auth configuration
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
class Meta:
db_table = 'users'
unique_together = [
('tenant', 'email'),
('tenant', 'username'),
]
indexes = [
models.Index(fields=['tenant', 'email']),
models.Index(fields=['tenant', 'username']),
]
def __str__(self):
return f"{self.email} ({self.tenant.name})"
@property
def is_owner(self):
"""Check if user is tenant owner."""
return self.role == UserRole.OWNER
@property
def is_admin(self):
"""Check if user is owner or admin."""
return self.role in [UserRole.OWNER, UserRole.ADMIN]
def has_permission(self, permission):
"""
Check tenant-specific permission.
Permissions format: 'resource.action' (e.g., 'users.create', 'projects.delete')
Args:
permission (str): Permission to check
Returns:
bool: True if user has permission
"""
role_permissions = {
UserRole.OWNER: ['*'], # All permissions
UserRole.ADMIN: [
'users.view', 'users.create', 'users.update', 'users.delete',
'projects.*',
'licenses.*'
],
UserRole.MEMBER: [
'projects.view', 'projects.create',
'licenses.view'
],
UserRole.VIEWER: [
'projects.view',
'licenses.view'
],
}
allowed = role_permissions.get(self.role, [])
# Check for wildcard (owner)
if '*' in allowed:
return True
# Check exact match
if permission in allowed:
return True
# Check resource wildcard (e.g., 'projects.*' matches 'projects.create')
for perm in allowed:
if perm.endswith('.*'):
resource = perm[:-2]
if permission.startswith(f"{resource}."):
return True
return False
Database Schema:
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
email VARCHAR(255) NOT NULL,
username VARCHAR(150) NOT NULL,
password VARCHAR(255) NOT NULL,
full_name VARCHAR(255),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_staff BOOLEAN NOT NULL DEFAULT FALSE,
role VARCHAR(50) NOT NULL DEFAULT 'member',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_login TIMESTAMPTZ,
UNIQUE(tenant_id, email),
UNIQUE(tenant_id, username)
);
CREATE INDEX idx_users_tenant ON users(tenant_id);
CREATE INDEX idx_users_email ON users(tenant_id, email);
CREATE INDEX idx_users_username ON users(tenant_id, username);
-- Row-Level Security
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY users_tenant_isolation ON users
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
LicenseSession Model
Module: apps.licenses.models
Purpose: Active license sessions with seat management and validation tracking.
Implementation:
from django.db import models
from apps.tenants.models import TenantModel
from apps.users.models import User
import uuid
class LicenseType(models.TextChoices):
FREE = 'free', 'Free'
PRO = 'pro', 'Pro'
TEAM = 'team', 'Team'
ENTERPRISE = 'enterprise', 'Enterprise'
class SessionStatus(models.TextChoices):
ACTIVE = 'active', 'Active'
EXPIRED = 'expired', 'Expired'
REVOKED = 'revoked', 'Revoked'
class LicenseSession(TenantModel):
"""
Active license session representing a seat in use.
Sessions have TTL and heartbeat tracking for automatic cleanup
of zombie sessions (6 min TTL in Redis).
"""
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False
)
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='license_sessions',
help_text="User who owns this session"
)
# Session details
session_token = models.CharField(
max_length=255,
unique=True,
db_index=True,
help_text="Unique session identifier (UUID)"
)
machine_id = models.CharField(
max_length=255,
help_text="Hardware fingerprint of client machine"
)
ip_address = models.GenericIPAddressField(
null=True,
blank=True,
help_text="Client IP address"
)
user_agent = models.TextField(
blank=True,
help_text="Client user agent string"
)
# License info
license_type = models.CharField(
max_length=50,
choices=LicenseType.choices,
help_text="License tier for this session"
)
features = models.JSONField(
default=list,
help_text="Enabled features list (JSON array)"
)
# Status
status = models.CharField(
max_length=20,
choices=SessionStatus.choices,
default=SessionStatus.ACTIVE
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField(
help_text="Session expiration time (8 hours from creation)"
)
last_validated_at = models.DateTimeField(
null=True,
blank=True,
help_text="Last heartbeat validation timestamp"
)
revoked_at = models.DateTimeField(
null=True,
blank=True,
help_text="When session was manually revoked"
)
# Metadata
metadata = models.JSONField(
default=dict,
help_text="Additional session metadata (JSON)"
)
class Meta:
db_table = 'license_sessions'
indexes = [
models.Index(fields=['tenant', 'status']),
models.Index(fields=['user']),
models.Index(fields=['session_token']),
models.Index(fields=['expires_at']),
]
def __str__(self):
return f"Session {self.session_token[:8]}... ({self.user.email})"
@property
def is_valid(self):
"""
Check if session is currently valid.
Returns:
bool: True if status is ACTIVE and not expired
"""
from django.utils import timezone
return (
self.status == SessionStatus.ACTIVE and
self.expires_at > timezone.now()
)
def validate(self):
"""
Validate session and update last_validated_at.
Called by heartbeat endpoint every 5 minutes.
Returns:
bool: True if session is valid and updated
"""
from django.utils import timezone
if not self.is_valid:
return False
self.last_validated_at = timezone.now()
self.save(update_fields=['last_validated_at'])
return True
def revoke(self):
"""
Revoke this session (release seat).
Sets status to REVOKED and records revoked_at timestamp.
"""
from django.utils import timezone
self.status = SessionStatus.REVOKED
self.revoked_at = timezone.now()
self.save(update_fields=['status', 'revoked_at'])
Database Schema:
CREATE TABLE license_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
session_token VARCHAR(255) NOT NULL UNIQUE,
machine_id VARCHAR(255) NOT NULL,
ip_address INET,
user_agent TEXT,
license_type VARCHAR(50) NOT NULL,
features JSONB NOT NULL DEFAULT '[]',
status VARCHAR(20) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
last_validated_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
metadata JSONB NOT NULL DEFAULT '{}'
);
CREATE INDEX idx_license_sessions_tenant ON license_sessions(tenant_id);
CREATE INDEX idx_license_sessions_user ON license_sessions(user_id);
CREATE INDEX idx_license_sessions_token ON license_sessions(session_token);
CREATE INDEX idx_license_sessions_status ON license_sessions(tenant_id, status);
CREATE INDEX idx_license_sessions_expires ON license_sessions(expires_at) WHERE status = 'active';
-- Row-Level Security
ALTER TABLE license_sessions ENABLE ROW LEVEL SECURITY;
CREATE POLICY license_sessions_tenant_isolation ON license_sessions
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
Project Model
Module: apps.projects.models
Purpose: User projects for organizing license usage and development work.
Implementation:
from django.db import models
from django.utils.text import slugify
from apps.tenants.models import TenantModel
from apps.users.models import User
import uuid
class ProjectStatus(models.TextChoices):
ACTIVE = 'active', 'Active'
ARCHIVED = 'archived', 'Archived'
class Project(TenantModel):
"""
User project within a tenant.
Projects are owned by users and can be archived/restored.
"""
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False
)
name = models.CharField(
max_length=255,
help_text="Project name"
)
slug = models.SlugField(
max_length=100,
help_text="URL-safe identifier (auto-generated from name)"
)
description = models.TextField(
blank=True,
help_text="Project description"
)
# Ownership
owner = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='owned_projects',
help_text="User who created this project"
)
# Settings
settings = models.JSONField(
default=dict,
help_text="Project-specific settings (JSON)"
)
# Status
status = models.CharField(
max_length=20,
choices=ProjectStatus.choices,
default=ProjectStatus.ACTIVE
)
archived_at = models.DateTimeField(
null=True,
blank=True,
help_text="When project was archived"
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'projects'
unique_together = [('tenant', 'slug')]
indexes = [
models.Index(fields=['tenant', 'slug']),
models.Index(fields=['owner']),
models.Index(fields=['tenant', 'status']),
]
def __str__(self):
return f"{self.name} ({self.tenant.name})"
def save(self, *args, **kwargs):
"""Auto-generate slug from name if not provided."""
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def archive(self):
"""Archive this project."""
from django.utils import timezone
self.status = ProjectStatus.ARCHIVED
self.archived_at = timezone.now()
self.save(update_fields=['status', 'archived_at'])
Database Schema:
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
owner_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
slug VARCHAR(100) NOT NULL,
description TEXT,
settings JSONB NOT NULL DEFAULT '{}',
status VARCHAR(20) NOT NULL DEFAULT 'active',
archived_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(tenant_id, slug)
);
CREATE INDEX idx_projects_tenant ON projects(tenant_id);
CREATE INDEX idx_projects_owner ON projects(owner_id);
CREATE INDEX idx_projects_slug ON projects(tenant_id, slug);
CREATE INDEX idx_projects_status ON projects(tenant_id, status);
-- Row-Level Security
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY projects_tenant_isolation ON projects
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
AuditLog Model
Module: apps.audit.models
Purpose: Comprehensive audit trail for compliance and security monitoring.
Implementation:
from django.db import models
from apps.tenants.models import TenantModel
from apps.users.models import User
class AuditLog(TenantModel):
"""
Audit log entry for tracking all tenant operations.
Logs all create/update/delete operations, login events, and
permission changes for compliance (SOC 2, GDPR, HIPAA).
"""
id = models.BigAutoField(primary_key=True)
# Actor
user = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='audit_logs',
help_text="User who performed the action (null if system)"
)
user_email = models.EmailField(
max_length=255,
blank=True,
help_text="Email at time of action (preserved even if user deleted)"
)
# Action
action = models.CharField(
max_length=100,
help_text="Action performed (create, update, delete, login, etc.)"
)
resource_type = models.CharField(
max_length=100,
help_text="Type of resource (user, project, license, etc.)"
)
resource_id = models.UUIDField(
null=True,
blank=True,
help_text="ID of affected resource"
)
# Details
changes = models.JSONField(
null=True,
blank=True,
help_text="Before/after changes (JSON)"
)
metadata = models.JSONField(
default=dict,
help_text="Additional context (JSON)"
)
# Context
ip_address = models.GenericIPAddressField(
null=True,
blank=True,
help_text="Client IP address"
)
user_agent = models.TextField(
blank=True,
help_text="Client user agent"
)
# Timestamp
created_at = models.DateTimeField(
auto_now_add=True,
help_text="When action occurred"
)
class Meta:
db_table = 'audit_logs'
ordering = ['-created_at']
indexes = [
models.Index(fields=['tenant', '-created_at']),
models.Index(fields=['user', '-created_at']),
models.Index(fields=['resource_type', 'resource_id']),
models.Index(fields=['tenant', 'action']),
]
def __str__(self):
return f"{self.action} on {self.resource_type} by {self.user_email or 'system'}"
@classmethod
def log(cls, action, resource_type, resource_id=None, user=None,
changes=None, metadata=None, request=None):
"""
Create an audit log entry.
Args:
action (str): Action performed (create, update, delete, login)
resource_type (str): Type of resource (user, project, license)
resource_id (UUID): ID of affected resource
user (User): User who performed action
changes (dict): Before/after changes
metadata (dict): Additional context
request (Request): Django request object (for IP, user agent)
Returns:
AuditLog: Created audit log entry
Example:
AuditLog.log(
action='create',
resource_type='project',
resource_id=project.id,
user=request.user,
metadata={'project_name': project.name},
request=request
)
"""
from apps.tenants.context import get_current_tenant
tenant = get_current_tenant()
if not tenant:
raise ValueError("Cannot create audit log without tenant context")
log_data = {
'tenant': tenant,
'action': action,
'resource_type': resource_type,
'resource_id': resource_id,
'user': user,
'user_email': user.email if user else '',
'changes': changes or {},
'metadata': metadata or {},
}
if request:
log_data['ip_address'] = get_client_ip(request)
log_data['user_agent'] = request.META.get('HTTP_USER_AGENT', '')
return cls.objects.create(**log_data)
def get_client_ip(request):
"""Extract client IP from request, handling proxies."""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
return x_forwarded_for.split(',')[0].strip()
return request.META.get('REMOTE_ADDR')
Database Schema:
CREATE TABLE audit_logs (
id BIGSERIAL PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
user_email VARCHAR(255),
action VARCHAR(100) NOT NULL,
resource_type VARCHAR(100) NOT NULL,
resource_id UUID,
changes JSONB,
metadata JSONB NOT NULL DEFAULT '{}',
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_audit_logs_tenant ON audit_logs(tenant_id, created_at DESC);
CREATE INDEX idx_audit_logs_user ON audit_logs(user_id, created_at DESC);
CREATE INDEX idx_audit_logs_resource ON audit_logs(resource_type, resource_id);
CREATE INDEX idx_audit_logs_action ON audit_logs(tenant_id, action);
-- Row-Level Security
ALTER TABLE audit_logs ENABLE ROW LEVEL SECURITY;
CREATE POLICY audit_logs_tenant_isolation ON audit_logs
USING (tenant_id = current_setting('app.current_tenant_id')::UUID);
Inheritance Hierarchy Summary
Inheritance Tree
BaseTenantModel (django-multitenant)
└── TenantModel (CODITECT custom)
├── User (also inherits AbstractBaseUser, PermissionsMixin)
├── LicenseSession
├── Project
└── AuditLog
Django Authentication
├── AbstractBaseUser
│ └── User
└── PermissionsMixin
└── User
Standalone (no tenant scoping)
└── Tenant
Field Inheritance
All TenantModel children inherit:
tenant: ForeignKey to Tenant (CASCADE delete)save(): Automatic tenant assignment from contextdelete(): Tenant context validation- Automatic query filtering by tenant
User additionally inherits from AbstractBaseUser:
password: Hashed password storagelast_login: Login trackingset_password(),check_password(): Password management
User additionally inherits from PermissionsMixin:
is_superuser: Django superuser flaggroups,user_permissions: Django permission systemhas_perm(),has_perms(): Permission checking
Query Examples
Automatic Tenant Filtering
from apps.tenants.context import set_current_tenant
from apps.users.models import User
from apps.projects.models import Project
# Set tenant context (done by middleware automatically)
tenant = Tenant.objects.get(slug='acme-corp')
set_current_tenant(tenant)
# All queries automatically filter to acme-corp
users = User.objects.all()
# SQL: SELECT * FROM users WHERE tenant_id = 'acme-corp-uuid'
active_projects = Project.objects.filter(status='active')
# SQL: SELECT * FROM projects WHERE tenant_id = 'acme-corp-uuid' AND status = 'active'
# Get by ID respects tenant filtering
try:
project = Project.objects.get(id='other-tenant-project-id')
# Raises DoesNotExist if project belongs to different tenant
except Project.DoesNotExist:
print("Cannot access other tenant's project")
Cross-Tenant Queries (Admin Only)
from apps.tenants.context import clear_current_tenant
# Clear tenant context to query across all tenants (admin only)
clear_current_tenant()
# Now queries return ALL data (no tenant filtering)
all_users = User.objects.all() # Returns users from ALL tenants
# SQL: SELECT * FROM users (no WHERE tenant_id clause)
# RLS still enforces isolation at PostgreSQL level unless connection is superuser
Related Object Access
set_current_tenant(my_tenant)
# Get user and their projects
user = User.objects.get(email='admin@example.com')
# Related manager automatically filters by tenant
projects = user.owned_projects.all()
# Returns only projects owned by user in current tenant
# Reverse relationship from tenant
users = my_tenant.users.filter(is_active=True)
sessions = my_tenant.license_sessions.filter(status='active')
Testing Examples
Unit Tests
from django.test import TestCase
from apps.tenants.models import Tenant
from apps.users.models import User
from apps.tenants.context import set_current_tenant
class TenantModelTestCase(TestCase):
def setUp(self):
self.tenant1 = Tenant.objects.create(name="Tenant 1", slug="tenant-1")
self.tenant2 = Tenant.objects.create(name="Tenant 2", slug="tenant-2")
def test_automatic_tenant_assignment(self):
"""Verify save() automatically assigns tenant from context."""
set_current_tenant(self.tenant1)
# Don't specify tenant explicitly
user = User(email="user@test.com", username="user1")
user.save()
# Tenant should be automatically assigned
self.assertEqual(user.tenant_id, self.tenant1.id)
def test_save_without_tenant_context_raises_error(self):
"""Verify save() raises ValueError without tenant context."""
clear_current_tenant()
user = User(email="user@test.com", username="user1")
with self.assertRaises(ValueError):
user.save()
Integration Tests
class TenantIsolationTestCase(TestCase):
def test_queryset_isolation(self):
"""Verify querysets respect tenant filtering."""
tenant1 = Tenant.objects.create(name="Tenant 1")
tenant2 = Tenant.objects.create(name="Tenant 2")
set_current_tenant(tenant1)
user1 = User.objects.create(email="user1@t1.com", username="user1")
set_current_tenant(tenant2)
user2 = User.objects.create(email="user2@t2.com", username="user2")
# Switch back to tenant1
set_current_tenant(tenant1)
# Should only see tenant1 users
users = User.objects.all()
self.assertEqual(users.count(), 1)
self.assertEqual(users.first().email, "user1@t1.com")
# Cannot access tenant2's user
with self.assertRaises(User.DoesNotExist):
User.objects.get(id=user2.id)
Related Documentation
- C3-01: Django Backend Components - Component architecture
- SUP-01: Entity-Relationship Diagram - Database schema
- ADR-007: Django Multi-Tenant Architecture - Complete specification
- PostgreSQL RLS: Row-Level Security Documentation
- django-multitenant: Library Documentation
Last Updated: 2025-11-30 Diagram Type: C4 Code (Mermaid Class Diagram) Scope: Model Layer - Django ORM with Multi-Tenant Architecture