Skip to main content

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:


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 tenant foreign 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 scoping
  • AbstractBaseUser - Django authentication
  • PermissionsMixin - 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 context
  • delete(): Tenant context validation
  • Automatic query filtering by tenant

User additionally inherits from AbstractBaseUser:

  • password: Hashed password storage
  • last_login: Login tracking
  • set_password(), check_password(): Password management

User additionally inherits from PermissionsMixin:

  • is_superuser: Django superuser flag
  • groups, user_permissions: Django permission system
  • has_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
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)


Last Updated: 2025-11-30 Diagram Type: C4 Code (Mermaid Class Diagram) Scope: Model Layer - Django ORM with Multi-Tenant Architecture