Files
igny8/docs/00-SYSTEM/TENANCY.md
IGNY8 VPS (Salman) 4bffede052 docs & ux improvmeents
2025-12-25 20:31:58 +00:00

7.0 KiB

Multi-Tenancy Architecture

Last Verified: December 25, 2025
Backend Path: backend/igny8_core/auth/models.py


Data Hierarchy

Account (Tenant)
├── Users (team members)
├── Subscription (plan binding)
├── Sites
│   ├── Sectors
│   │   ├── Keywords
│   │   ├── Clusters
│   │   ├── ContentIdeas
│   │   ├── Tasks
│   │   ├── Content
│   │   └── Images
│   └── Integrations (WordPress)
└── Billing (credits, transactions)

Core Models

Account

Field Type Purpose
name CharField Account/organization name
is_active Boolean Enable/disable account
credits Decimal Current credit balance
stripe_customer_id CharField Stripe integration
paypal_customer_id CharField PayPal integration
created_at DateTime Registration date

Plan

Field Type Purpose
name CharField Plan name (Free, Starter, Growth, Scale)
included_credits Integer Monthly credit allocation
max_sites Integer Site limit
max_users Integer User limit
max_keywords Integer Keyword limit
max_clusters Integer Cluster limit
max_content_ideas Integer Monthly idea limit
max_content_words Integer Monthly word limit
max_images_basic Integer Monthly basic image limit
max_images_premium Integer Monthly premium image limit
is_active Boolean Available for purchase
is_internal Boolean Internal/test plan

Site

Field Type Purpose
account ForeignKey Owner account
name CharField Site name
domain CharField Primary domain
is_active Boolean Enable/disable

Sector

Field Type Purpose
site ForeignKey Parent site
account ForeignKey Owner account
industry ForeignKey Industry template
name CharField Sector name
is_active Boolean Enable/disable

Base Model Classes

AccountBaseModel

File: auth/models.py

All account-scoped models inherit from this:

class AccountBaseModel(models.Model):
    account = models.ForeignKey(Account, on_delete=models.CASCADE)
    
    class Meta:
        abstract = True

Used by: Billing records, Settings, API keys

SiteSectorBaseModel

File: auth/models.py

All content models inherit from this:

class SiteSectorBaseModel(AccountBaseModel):
    site = models.ForeignKey(Site, on_delete=models.CASCADE)
    sector = models.ForeignKey(Sector, on_delete=models.CASCADE)
    
    class Meta:
        abstract = True

Used by: Keywords, Clusters, Ideas, Tasks, Content, Images


ViewSet Base Classes

AccountModelViewSet

File: api/base.py

Automatically filters queryset by account:

class AccountModelViewSet(ModelViewSet):
    def get_queryset(self):
        qs = super().get_queryset()
        if not self.request.user.is_admin_or_developer:
            qs = qs.filter(account=self.request.account)
        return qs

SiteSectorModelViewSet

File: api/base.py

Filters by account + site + sector:

class SiteSectorModelViewSet(AccountModelViewSet):
    def get_queryset(self):
        qs = super().get_queryset()
        site_id = self.request.query_params.get('site_id')
        sector_id = self.request.query_params.get('sector_id')
        if site_id:
            qs = qs.filter(site_id=site_id)
        if sector_id:
            qs = qs.filter(sector_id=sector_id)
        return qs

Where Tenancy Applies

Uses Site/Sector Filtering

Module Filter
Planner (Keywords, Clusters, Ideas) site + sector
Writer (Tasks, Content, Images) site + sector
Linker site + sector
Optimizer site + sector
Setup/Add Keywords site + sector

Account-Level Only (No Site/Sector)

Module Filter
Billing/Plans account only
Account Settings account only
Team Management account only
User Profile user only
System Settings account only

Frontend Implementation

Site Selection

Store: store/siteStore.ts

const useSiteStore = create({
  activeSite: Site | null,
  sites: Site[],
  loadSites: () => Promise<void>,
  setActiveSite: (site: Site) => void,
});

Sector Selection

Store: store/sectorStore.ts

const useSectorStore = create({
  activeSector: Sector | null,
  sectors: Sector[],
  loadSectorsForSite: (siteId: number) => Promise<void>,
  setActiveSector: (sector: Sector) => void,
});

Sector Loading Pattern

Sectors are loaded by PageHeader component, not AppLayout:

// PageHeader.tsx
useEffect(() => {
  if (hideSiteSector) return; // Skip for account pages
  
  if (activeSite?.id && activeSite?.is_active) {
    loadSectorsForSite(activeSite.id);
  }
}, [activeSite?.id, hideSiteSector]);

Pages with hideSiteSector={true}:

  • /account/* (settings, team, billing)
  • User profile pages

Global Resources (No Tenancy)

These are system-wide, not tenant-specific:

Model Purpose
Industry Global industry taxonomy
IndustrySector Sub-categories within industries
SeedKeyword Global keyword database
GlobalIntegrationSettings Platform API keys
GlobalAIPrompt Default prompt templates
GlobalAuthorProfile Author persona templates

Tenant Data vs System Data

System Data (KEEP on reset)

  • Plans, CreditPackages
  • Industries, IndustrySectors
  • GlobalIntegrationSettings
  • GlobalAIPrompt, GlobalAuthorProfile
  • CreditCostConfig
  • PaymentMethodConfig

Tenant Data (CLEAN on reset)

  • Accounts, Users, Sites, Sectors
  • Keywords, Clusters, Ideas, Tasks, Content, Images
  • Subscriptions, Invoices, Payments
  • CreditTransactions, CreditUsageLogs
  • SiteIntegrations, SyncEvents
  • AutomationConfigs, AutomationRuns

Admin/Developer Bypass

Admin and developer users can access all tenants:

# In auth/models.py
class User:
    @property
    def is_admin_or_developer(self):
        return self.role in ['admin', 'developer']

Bypass Rules:

  • Admin: Full access to own account
  • Developer: Full access to ALL accounts
  • System accounts protected from deletion

Common Issues

Issue Cause Fix
Data leak between accounts Missing account filter Use AccountModelViewSet
Wrong site data Site not validated for account Validate site.account == request.account
Sector not loading Site changed but sector not reset Clear activeSector when activeSite changes
Admin can't see all data Incorrect role check Check is_admin_or_developer

Planned Changes

Feature Status Description
Account switching 🔜 Planned Allow users to switch between accounts
Sub-accounts 🔜 Planned Hierarchical account structure