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_premiumwere 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 |