docs re-org
This commit is contained in:
516
ARCHITECTURE-KNOWLEDGE-BASE.md
Normal file
516
ARCHITECTURE-KNOWLEDGE-BASE.md
Normal file
@@ -0,0 +1,516 @@
|
||||
# Architecture Knowledge Base
|
||||
**Last Updated:** December 8, 2025
|
||||
**Purpose:** Critical architectural patterns, common issues, and solutions reference
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
1. [Authentication & Session Management](#authentication--session-management)
|
||||
2. [Site/Sector Architecture](#sitesector-architecture)
|
||||
3. [State Management & Race Conditions](#state-management--race-conditions)
|
||||
4. [Permission System](#permission-system)
|
||||
5. [Frontend Component Dependencies](#frontend-component-dependencies)
|
||||
6. [Common Pitfalls & Solutions](#common-pitfalls--solutions)
|
||||
|
||||
---
|
||||
|
||||
## Authentication & Session Management
|
||||
|
||||
### Token Persistence Architecture
|
||||
|
||||
**Problem Pattern:**
|
||||
- Zustand persist middleware writes to localStorage asynchronously
|
||||
- API calls can happen before tokens are persisted
|
||||
- Results in 403 "Authentication credentials were not provided" errors
|
||||
|
||||
**Solution Implemented:**
|
||||
```typescript
|
||||
// In authStore.ts login/register functions
|
||||
// CRITICAL: Immediately persist tokens synchronously after setting state
|
||||
const authState = {
|
||||
state: { user, token, refreshToken, isAuthenticated: true },
|
||||
version: 0
|
||||
};
|
||||
localStorage.setItem('auth-storage', JSON.stringify(authState));
|
||||
```
|
||||
|
||||
**Key Principle:** Always write tokens to localStorage synchronously in auth actions, don't rely solely on persist middleware.
|
||||
|
||||
---
|
||||
|
||||
### Logout & State Cleanup
|
||||
|
||||
**WRONG APPROACH (causes race conditions):**
|
||||
```typescript
|
||||
logout: () => {
|
||||
localStorage.clear(); // ❌ BREAKS EVERYTHING
|
||||
set({ user: null, token: null });
|
||||
}
|
||||
```
|
||||
|
||||
**CORRECT APPROACH:**
|
||||
```typescript
|
||||
logout: () => {
|
||||
// ✅ Selective removal - only auth-related keys
|
||||
const authKeys = ['auth-storage', 'site-storage', 'sector-storage', 'billing-storage'];
|
||||
authKeys.forEach(key => localStorage.removeItem(key));
|
||||
|
||||
// ✅ Reset dependent stores explicitly
|
||||
useSiteStore.setState({ activeSite: null });
|
||||
useSectorStore.setState({ activeSector: null, sectors: [] });
|
||||
|
||||
set({ user: null, token: null, isAuthenticated: false });
|
||||
}
|
||||
```
|
||||
|
||||
**Key Principle:** Never use `localStorage.clear()` - it breaks Zustand persist middleware initialization. Always selectively remove keys.
|
||||
|
||||
---
|
||||
|
||||
### 403 Error Handling
|
||||
|
||||
**Problem Pattern:**
|
||||
- 403 errors thrown before checking if it's an auth error
|
||||
- Token validation code becomes unreachable
|
||||
- Invalid tokens persist in localStorage
|
||||
|
||||
**WRONG ORDER:**
|
||||
```typescript
|
||||
// In api.ts
|
||||
if (response.status === 403) {
|
||||
throw new Error(response.statusText); // ❌ Thrown immediately
|
||||
}
|
||||
|
||||
// This code NEVER runs (unreachable):
|
||||
if (errorData?.detail?.includes('Authentication credentials')) {
|
||||
logout(); // Never called!
|
||||
}
|
||||
```
|
||||
|
||||
**CORRECT ORDER:**
|
||||
```typescript
|
||||
// Check for auth errors FIRST, then throw
|
||||
if (response.status === 403) {
|
||||
const errorData = JSON.parse(text);
|
||||
|
||||
// ✅ Check authentication BEFORE throwing
|
||||
if (errorData?.detail?.includes('Authentication credentials')) {
|
||||
const authState = useAuthStore.getState();
|
||||
if (authState?.isAuthenticated) {
|
||||
authState.logout();
|
||||
window.location.href = '/signin';
|
||||
}
|
||||
}
|
||||
|
||||
// Now throw the error
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
```
|
||||
|
||||
**Key Principle:** Handle authentication errors before throwing. Order matters in error handling logic.
|
||||
|
||||
---
|
||||
|
||||
## Site/Sector Architecture
|
||||
|
||||
### Data Hierarchy
|
||||
```
|
||||
Account (Tenant)
|
||||
└── Site (e.g., myblog.com)
|
||||
└── Sector (e.g., Technology, Health)
|
||||
└── Keywords
|
||||
└── Clusters
|
||||
└── Ideas
|
||||
└── Content
|
||||
```
|
||||
|
||||
### Where Sectors Are Used (Global Context)
|
||||
|
||||
**USES SECTORS (requires site/sector selection):**
|
||||
- ✅ Planner Module (Keywords, Clusters, Ideas)
|
||||
- ✅ Writer Module (Tasks, Content, Drafts, Published)
|
||||
- ✅ Linker Module (Internal linking)
|
||||
- ✅ Optimizer Module (Content optimization)
|
||||
- ✅ Setup/Add Keywords page
|
||||
- ✅ Seed Keywords reference data
|
||||
|
||||
**DOES NOT USE SECTORS (account-level only):**
|
||||
- ❌ Billing/Plans pages (`/account/*`)
|
||||
- ❌ Account Settings
|
||||
- ❌ Team Management
|
||||
- ❌ User Profile
|
||||
- ❌ Admin Dashboard
|
||||
- ❌ System Settings
|
||||
|
||||
### Sector Loading Pattern
|
||||
|
||||
**Architecture Decision:**
|
||||
- Sectors loaded by **PageHeader component** (not AppLayout)
|
||||
- Only loads when `hideSiteSector={false}` prop is set
|
||||
- Account/billing pages pass `hideSiteSector={true}` to skip loading
|
||||
|
||||
**Implementation:**
|
||||
```typescript
|
||||
// PageHeader.tsx
|
||||
useEffect(() => {
|
||||
if (hideSiteSector) return; // Skip for account pages
|
||||
|
||||
const currentSiteId = activeSite?.id ?? null;
|
||||
if (currentSiteId && activeSite?.is_active) {
|
||||
loadSectorsForSite(currentSiteId);
|
||||
}
|
||||
}, [activeSite?.id, hideSiteSector]);
|
||||
```
|
||||
|
||||
**Key Principle:** Lazy-load sectors only when components need them. Don't load globally for all pages.
|
||||
|
||||
---
|
||||
|
||||
### Site/Sector Store Persistence
|
||||
|
||||
**Storage Keys:**
|
||||
- `site-storage` - Active site selection
|
||||
- `sector-storage` - Active sector selection
|
||||
|
||||
**Reset Pattern:**
|
||||
```typescript
|
||||
// When site changes, reset sector if it belongs to different site
|
||||
if (currentSector && currentSector.site_id !== newSiteId) {
|
||||
set({ activeSector: null });
|
||||
localStorage.setItem('sector-storage', JSON.stringify({
|
||||
state: { activeSector: null },
|
||||
version: 0
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
**Key Principle:** Sector selection is site-scoped. Always validate sector belongs to active site.
|
||||
|
||||
---
|
||||
|
||||
## State Management & Race Conditions
|
||||
|
||||
### Common Race Condition Patterns
|
||||
|
||||
#### 1. User Switching
|
||||
**Problem:** Rapid logout → login leaves stale state in stores
|
||||
|
||||
**Solution:**
|
||||
```typescript
|
||||
logout: () => {
|
||||
// Reset ALL dependent stores explicitly
|
||||
import('./siteStore').then(({ useSiteStore }) => {
|
||||
useSiteStore.setState({ activeSite: null, loading: false, error: null });
|
||||
});
|
||||
import('./sectorStore').then(({ useSectorStore }) => {
|
||||
useSectorStore.setState({ activeSector: null, sectors: [], loading: false, error: null });
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. API Calls Before Token Persistence
|
||||
**Problem:** API calls happen before Zustand persist writes token
|
||||
|
||||
**Solution:** Synchronous localStorage write immediately after state update (see Authentication section)
|
||||
|
||||
#### 3. Module Loading Failures
|
||||
**Problem:** 404 errors during page navigation cause module loading to fail
|
||||
|
||||
**Solution:** Ensure API endpoints exist before pages try to load them. Use conditional rendering based on route.
|
||||
|
||||
---
|
||||
|
||||
### Zustand Persist Middleware Gotchas
|
||||
|
||||
**Issue 1: Version Mismatch**
|
||||
```typescript
|
||||
// Stored format
|
||||
{ state: { user, token }, version: 0 }
|
||||
|
||||
// If version changes, persist middleware clears state
|
||||
```
|
||||
|
||||
**Issue 2: Async Hydration**
|
||||
- State rehydration from localStorage is async
|
||||
- Can cause brief flash of "no user" state
|
||||
|
||||
**Solution:** Use loading states or check both store AND localStorage:
|
||||
```typescript
|
||||
const getAuthToken = (): string | null => {
|
||||
// Try Zustand store first
|
||||
const authState = useAuthStore.getState();
|
||||
if (authState?.token) return authState.token;
|
||||
|
||||
// Fallback to localStorage
|
||||
const stored = localStorage.getItem('auth-storage');
|
||||
return JSON.parse(stored)?.state?.token || null;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Permission System
|
||||
|
||||
### Superuser/Developer Bypass Pattern
|
||||
|
||||
**Critical Locations for Bypass:**
|
||||
1. Middleware - `auth/middleware.py`
|
||||
2. Permission Classes - `api/permissions.py`
|
||||
3. ViewSet Querysets - `api/base.py`
|
||||
4. Validation Functions - `auth/utils.py`
|
||||
|
||||
**Standard Bypass Check:**
|
||||
```python
|
||||
def check_bypass(user):
|
||||
return (
|
||||
user.is_superuser or
|
||||
user.role == 'developer' or
|
||||
is_system_account_user(user)
|
||||
)
|
||||
```
|
||||
|
||||
**Apply at ALL levels:**
|
||||
- Middleware request validation
|
||||
- DRF permission `has_permission()`
|
||||
- ViewSet `get_queryset()` filtering
|
||||
- Custom validation functions
|
||||
|
||||
**Key Principle:** Bypass checks must be consistent across all permission layers. Missing one layer breaks superuser access.
|
||||
|
||||
---
|
||||
|
||||
### System Account Pattern
|
||||
|
||||
**Reserved Accounts:**
|
||||
- `aws-admin` - System automation account
|
||||
- `default-account` - Default tenant fallback
|
||||
|
||||
**Check Function:**
|
||||
```python
|
||||
def is_system_account_user(user):
|
||||
if not user or not user.account:
|
||||
return False
|
||||
return user.account.slug in ['aws-admin', 'default-account']
|
||||
```
|
||||
|
||||
**Usage:** Always include in bypass checks alongside superuser/developer.
|
||||
|
||||
---
|
||||
|
||||
## Frontend Component Dependencies
|
||||
|
||||
### PageHeader Component
|
||||
**Dependencies:**
|
||||
- `useSiteStore` - Active site
|
||||
- `useSectorStore` - Active sector
|
||||
- `SiteAndSectorSelector` - Dropdown component
|
||||
|
||||
**Props:**
|
||||
- `hideSiteSector: boolean` - Skip site/sector display and loading
|
||||
- `title: string` - Page title
|
||||
- `navigation: ReactNode` - Optional module tabs
|
||||
|
||||
**Used By:**
|
||||
- All Planner pages
|
||||
- All Writer pages
|
||||
- All Optimizer pages
|
||||
- Setup pages
|
||||
- Seed Keywords page
|
||||
|
||||
**NOT Used By:**
|
||||
- Account/billing pages (use plain headers instead)
|
||||
|
||||
---
|
||||
|
||||
### Module Navigation Pattern
|
||||
|
||||
**Component:** `ModuleNavigationTabs.tsx`
|
||||
|
||||
**CRITICAL:** Must be wrapped in `<Router>` context
|
||||
- Uses `useLocation()` and `useNavigate()` hooks
|
||||
- Cannot be used outside `<Routes>` tree
|
||||
|
||||
**Common Error:**
|
||||
```
|
||||
Error: useLocation() may be used only in the context of a <Router> component
|
||||
```
|
||||
|
||||
**Cause:** Component rendered outside React Router context
|
||||
|
||||
**Solution:** Ensure component is within `<Route>` element in App.tsx
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls & Solutions
|
||||
|
||||
### Pitfall 1: Frontend 403 Errors After User Switch
|
||||
|
||||
**Symptoms:**
|
||||
- "Authentication credentials were not provided"
|
||||
- User appears logged in but API calls fail
|
||||
- Manually clearing cache fixes it
|
||||
|
||||
**Root Cause:** Invalid tokens persisting in localStorage after logout
|
||||
|
||||
**Solution:**
|
||||
1. Check 403 handler runs BEFORE throwing error
|
||||
2. Ensure logout clears specific auth keys (not `localStorage.clear()`)
|
||||
3. Add immediate token persistence after login
|
||||
|
||||
**Prevention:** See "Authentication & Session Management" section
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 2: Sector 404 Errors on Billing Pages
|
||||
|
||||
**Symptoms:**
|
||||
- `GET /v1/auth/sites/{id}/sectors/` returns 404
|
||||
- "Failed to fetch dynamically imported module" error
|
||||
- Billing pages don't load
|
||||
|
||||
**Root Cause:** AppLayout loading sectors for ALL pages globally
|
||||
|
||||
**Solution:** Move sector loading to PageHeader component (lazy loading)
|
||||
|
||||
**Prevention:** Only load data when components that need it are mounted
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 3: Module Loading Failures After Git Commits
|
||||
|
||||
**Symptoms:**
|
||||
- React Router context errors
|
||||
- "useLocation() may be used only in context of <Router>" errors
|
||||
- Pages work after rebuild but fail after git push
|
||||
|
||||
**Root Cause:** Docker build cache not invalidated properly
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Force clean rebuild
|
||||
docker compose -f docker-compose.app.yml down
|
||||
docker compose -f docker-compose.app.yml build --no-cache igny8_frontend
|
||||
docker compose -f docker-compose.app.yml up -d
|
||||
```
|
||||
|
||||
**Prevention:** Use `--no-cache` flag when rebuilding after major changes
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 4: Plan Selection Issues in Pricing Page
|
||||
|
||||
**Symptoms:**
|
||||
- Monthly/Annual toggle missing
|
||||
- Pre-selected plan not highlighted
|
||||
- Discount calculation wrong
|
||||
|
||||
**Root Cause:**
|
||||
1. PricingTable component missing `showToggle` prop
|
||||
2. Backend missing `is_featured` and `annual_discount_percent` fields
|
||||
3. Frontend not calculating annual price from discount
|
||||
|
||||
**Solution:**
|
||||
1. Add fields to Plan model with migration
|
||||
2. Pass `annualDiscountPercent` to PricingTable
|
||||
3. Calculate: `annualPrice = monthlyPrice * 12 * (1 - discount/100)`
|
||||
|
||||
**Files Modified:**
|
||||
- `backend/igny8_core/auth/models.py`
|
||||
- `backend/igny8_core/auth/serializers.py`
|
||||
- `frontend/src/services/billing.api.ts`
|
||||
- `frontend/src/components/ui/pricing-table/PricingTable.tsx`
|
||||
|
||||
---
|
||||
|
||||
### Pitfall 5: Adjacent JSX Elements Error
|
||||
|
||||
**Symptoms:**
|
||||
- "Adjacent JSX elements must be wrapped in an enclosing tag"
|
||||
- Build fails but line numbers don't help
|
||||
|
||||
**Root Cause:** Mismatched opening/closing tags (usually missing `</div>`)
|
||||
|
||||
**Debugging Strategy:**
|
||||
1. Use TypeScript compiler: `npx tsc --noEmit <file>`
|
||||
2. Count opening vs closing tags: `grep -c "<div" vs grep -c "</div>"`
|
||||
3. Check conditionals have matching closing parens/braces
|
||||
|
||||
**Common Pattern:**
|
||||
```tsx
|
||||
{condition && (
|
||||
<div>
|
||||
{/* Content */}
|
||||
</div>
|
||||
{/* Missing closing parenthesis causes "adjacent elements" error */}
|
||||
}
|
||||
```
|
||||
|
||||
**Solution:** Ensure every opening bracket has matching close bracket
|
||||
|
||||
---
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
### State Management
|
||||
✅ **DO:** Immediately persist auth tokens synchronously
|
||||
✅ **DO:** Selectively remove localStorage keys
|
||||
✅ **DO:** Reset dependent stores on logout
|
||||
❌ **DON'T:** Use `localStorage.clear()`
|
||||
❌ **DON'T:** Rely solely on Zustand persist middleware timing
|
||||
|
||||
### Error Handling
|
||||
✅ **DO:** Check authentication errors BEFORE throwing
|
||||
✅ **DO:** Force logout on invalid tokens
|
||||
✅ **DO:** Redirect to login after logout
|
||||
❌ **DON'T:** Throw errors before checking auth status
|
||||
❌ **DON'T:** Leave invalid tokens in storage
|
||||
|
||||
### Component Architecture
|
||||
✅ **DO:** Lazy-load data at component level
|
||||
✅ **DO:** Skip unnecessary data loading (hideSiteSector pattern)
|
||||
✅ **DO:** Keep components in Router context
|
||||
❌ **DON'T:** Load data globally in AppLayout
|
||||
❌ **DON'T:** Use Router hooks outside Router context
|
||||
|
||||
### Permission System
|
||||
✅ **DO:** Implement bypass at ALL permission layers
|
||||
✅ **DO:** Include system accounts in bypass checks
|
||||
✅ **DO:** Use consistent bypass logic everywhere
|
||||
❌ **DON'T:** Forget middleware layer bypass
|
||||
❌ **DON'T:** Mix permission approaches
|
||||
|
||||
### Docker Builds
|
||||
✅ **DO:** Use `--no-cache` after major changes
|
||||
✅ **DO:** Restart containers after rebuilds
|
||||
✅ **DO:** Check logs for module loading errors
|
||||
❌ **DON'T:** Trust build cache after git commits
|
||||
❌ **DON'T:** Deploy without testing fresh build
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: File Locations
|
||||
|
||||
### Authentication
|
||||
- Token handling: `frontend/src/services/api.ts`
|
||||
- Auth store: `frontend/src/store/authStore.ts`
|
||||
- Middleware: `backend/igny8_core/auth/middleware.py`
|
||||
|
||||
### Permissions
|
||||
- Permission classes: `backend/igny8_core/api/permissions.py`
|
||||
- Base viewsets: `backend/igny8_core/api/base.py`
|
||||
- Validation utils: `backend/igny8_core/auth/utils.py`
|
||||
|
||||
### Site/Sector
|
||||
- Site store: `frontend/src/store/siteStore.ts`
|
||||
- Sector store: `frontend/src/store/sectorStore.ts`
|
||||
- PageHeader: `frontend/src/components/common/PageHeader.tsx`
|
||||
|
||||
### Billing
|
||||
- Billing API: `frontend/src/services/billing.api.ts`
|
||||
- Plans page: `frontend/src/pages/account/PlansAndBillingPage.tsx`
|
||||
- Plan model: `backend/igny8_core/auth/models.py`
|
||||
|
||||
---
|
||||
|
||||
**End of Knowledge Base**
|
||||
*Update this document when architectural patterns change or new common issues are discovered.*
|
||||
Reference in New Issue
Block a user