Files
igny8/docs/00-SYSTEM/TENANCY.md
IGNY8 VPS (Salman) c777e5ccb2 dos updates
2026-01-20 14:45:21 +00:00

7.3 KiB

Multi-Tenancy Architecture

Last Verified: January 20, 2026
Version: 1.8.4
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 Plan credits (reset on renewal)
bonus_credits Decimal Purchased credits (never expire)
account_timezone CharField IANA timezone (e.g., America/New_York)
stripe_customer_id CharField Stripe integration
paypal_customer_id CharField PayPal integration
created_at DateTime Registration date

Two-Pool Credit System (v1.8.3): Plan credits consumed first, bonus credits only when plan = 0. Bonus credits never expire.

Plan

Field Type Purpose
name CharField Plan name (Free, Starter, Growth, Scale)
included_credits Integer Monthly credit allocation
max_sites Integer Site limit (hard limit)
max_users Integer User limit (hard limit)
max_keywords Integer Keyword limit (hard limit)
max_ahrefs_queries Integer Monthly Ahrefs queries (monthly limit)
is_active Boolean Available for purchase
is_internal Boolean Internal/test plan

Note (v1.5.0+): Operation limits like max_clusters, max_content_words, max_images_basic, max_images_premium were removed. All AI operations are now credit-based.

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