docs & ux improvmeents
This commit is contained in:
299
docs/00-SYSTEM/TENANCY.md
Normal file
299
docs/00-SYSTEM/TENANCY.md
Normal file
@@ -0,0 +1,299 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user