# 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:

```python
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:

```python
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:

```python
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:

```python
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`

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

### Sector Selection

**Store:** `store/sectorStore.ts`

```typescript
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`:

```typescript
// 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:

```python
# 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 |
