- Moved DESIGN-GUIDE.md → docs/30-FRONTEND/DESIGN-GUIDE.md - Moved frontend/DESIGN_SYSTEM.md → docs/30-FRONTEND/DESIGN-TOKENS.md - Moved IGNY8-APP.md → docs/00-SYSTEM/IGNY8-APP.md - Moved fixes-kb.md → docs/90-REFERENCE/FIXES-KB.md - Moved FINAL_PRELAUNCH.md → docs/plans/FINAL-PRELAUNCH.md - Updated all references in .rules, README.md, docs/INDEX.md - Updated ESLint plugin documentation comments - Root folder now only contains: .rules, CHANGELOG.md, README.md
16 KiB
Architecture Knowledge Base
Last Updated: December 14, 2025
Purpose: Critical architectural patterns, common issues, and solutions reference
🔥 CRITICAL FIXES - December 2025
PERMANENT FIX: Django Admin Custom Sidebar Not Showing on Subpages
ROOT CAUSE: Django's ModelAdmin view methods (changelist_view, change_view, etc.) do not call AdminSite.each_context(), so custom sidebar logic defined in site.py was bypassed on model list/detail/edit pages.
SOLUTION IMPLEMENTED:
- ✅ Created
Igny8ModelAdminbase class extendingUnfoldModelAdmin - ✅ Overrides all view methods to inject
extra_contextwith custom sidebar - ✅ Applied to 46+ admin classes across all modules
- ✅ Sidebar now consistent on homepage, app index, and ALL model pages
Files Modified: backend/igny8_core/admin/base.py, all */admin.py files
PERMANENT FIX: User Swapping / Random Logout Issue
ROOT CAUSE: Django's database-backed sessions with in-memory user caching caused cross-request contamination at the process level.
SOLUTION IMPLEMENTED:
- ✅ Redis-backed sessions (
SESSION_ENGINE = 'django.contrib.sessions.backends.cache') - ✅ Custom authentication backend without caching (
NoCacheModelBackend) - ✅ Session integrity validation (stores and verifies account_id/user_id on every request)
- ✅ Middleware never mutates
request.user(uses Django's set value directly)
See: CRITICAL-BUG-FIXES-DEC-2025.md for complete details.
PERMANENT FIX: useNavigate / useLocation Errors During HMR
ROOT CAUSE: Individual Suspense boundaries per route lost React Router context during Hot Module Replacement.
SOLUTION IMPLEMENTED:
- ✅ Single top-level Suspense boundary around entire
<Routes>component - ✅ Removed 100+ individual Suspense wrappers from route elements
- ✅ Router context now persists through HMR automatically
See: CRITICAL-BUG-FIXES-DEC-2025.md for complete details.
Table of Contents
- Authentication & Session Management
- Site/Sector Architecture
- State Management & Race Conditions
- Permission System
- Frontend Component Dependencies
- 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:
// 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):
logout: () => {
localStorage.clear(); // ❌ BREAKS EVERYTHING
set({ user: null, token: null });
}
CORRECT APPROACH:
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:
// 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:
// 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:
// 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 selectionsector-storage- Active sector selection
Reset Pattern:
// 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:
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
// 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:
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:
- Middleware -
auth/middleware.py - Permission Classes -
api/permissions.py - ViewSet Querysets -
api/base.py - Validation Functions -
auth/utils.py
Standard Bypass Check:
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 accountdefault-account- Default tenant fallback
Check Function:
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 siteuseSectorStore- Active sectorSiteAndSectorSelector- Dropdown component
Props:
hideSiteSector: boolean- Skip site/sector display and loadingtitle: string- Page titlenavigation: 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()anduseNavigate()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:
- Check 403 handler runs BEFORE throwing error
- Ensure logout clears specific auth keys (not
localStorage.clear()) - 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 " errors
- Pages work after rebuild but fail after git push
Root Cause: Docker build cache not invalidated properly
Solution:
# 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:
- PricingTable component missing
showToggleprop - Backend missing
is_featuredandannual_discount_percentfields - Frontend not calculating annual price from discount
Solution:
- Add fields to Plan model with migration
- Pass
annualDiscountPercentto PricingTable - Calculate:
annualPrice = monthlyPrice * 12 * (1 - discount/100)
Files Modified:
backend/igny8_core/auth/models.pybackend/igny8_core/auth/serializers.pyfrontend/src/services/billing.api.tsfrontend/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:
- Use TypeScript compiler:
npx tsc --noEmit <file> - Count opening vs closing tags:
grep -c "<div" vs grep -c "</div>" - Check conditionals have matching closing parens/braces
Common Pattern:
{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.