Detailed explanation of the Odoo PWA architecture, patterns, and design decisions. ## What this command does: - Explains the architectural patterns used in generated PWAs - Details the data flow and state management - Describes the caching strategy - Explains offline-first design principles - Provides insights into technical decisions --- ## Architecture Overview 🏗️ The Odoo PWA Generator creates **offline-first Progressive Web Apps** with a **three-layer architecture** that ensures data availability, performance, and seamless Odoo integration. ``` ┌─────────────────────────────────────────┐ │ UI Layer (Components) │ │ - Forms, Lists, Navigation │ │ - Framework-specific (Svelte/React/Vue)│ └──────────────┬──────────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ State Layer (Cache Stores) │ │ - Smart Caching Logic │ │ - Dual Storage (localStorage + IndexedDB) │ │ - Background Sync │ │ - Optimistic Updates │ └──────────────┬──────────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ API Layer (Odoo Client) │ │ - JSON-RPC Communication │ │ - CRUD Operations │ │ - Field Formatting │ └──────────────┬──────────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ Server Layer (API Routes) │ │ - Credential Management │ │ - UID Caching │ │ - Error Handling │ └──────────────┬──────────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ Odoo Backend │ │ - Studio Models │ │ - Business Logic │ │ - Data Persistence │ └─────────────────────────────────────────┘ ``` --- ## Layer 1: UI Components 🎨 ### Purpose: Present data to users and capture user input. ### Responsibilities: - Render data from cache stores - Handle user interactions - Validate form inputs - Display loading and error states - Provide responsive, mobile-friendly interface ### Framework-Specific Implementation: #### SvelteKit ```javascript {#each $expenseCache as expense} {/each} ``` #### React ```javascript import { useExpense } from './contexts/ExpenseContext'; function ExpenseList() { const { records, isLoading } = useExpense(); useEffect(() => { // Load on mount }, []); return records.map(expense => ( )); } ``` #### Vue ```javascript ``` ### Design Principles: - **Reactive by default** - UI updates automatically when data changes - **Loading states** - Show skeleton loaders while data fetches - **Error boundaries** - Graceful error handling - **Optimistic UI** - Show changes immediately, sync in background --- ## Layer 2: Cache Stores (State Management) 💾 ### Purpose: Manage application state with offline-first caching. ### The Smart Caching Pattern: ```javascript // Two-phase load strategy export async function load() { // Phase 1: Load from cache (instant) const cached = await loadFromCache(); records.set(cached); // UI shows data immediately // Phase 2: Check if stale and sync if (isCacheStale()) { await syncInBackground(); // Update in background } } ``` ### Dual Storage Strategy: #### localStorage (Metadata) Stores lightweight metadata: - `lastSyncTime` - When was last successful sync - `lastRecordId` - Highest ID fetched so far - `version` - Cache schema version ```javascript localStorage.setItem('expenseCache', JSON.stringify({ lastSyncTime: Date.now(), lastRecordId: 123, version: 1 })); ``` #### IndexedDB (Master Data) Stores actual records: - All record data - Larger storage capacity - Async API - Structured data ```javascript await db.expenses.bulkPut(records); ``` ### Stale Detection: ```javascript function isCacheStale() { const cacheData = JSON.parse(localStorage.getItem('expenseCache')); const CACHE_VALIDITY = 5 * 60 * 1000; // 5 minutes return !cacheData || (Date.now() - cacheData.lastSyncTime) > CACHE_VALIDITY; } ``` ### Incremental Sync Pattern: ```javascript // Only fetch new records, not everything async function syncInBackground() { const { lastRecordId } = getCacheMetadata(); // Fetch only records with id > lastRecordId const newRecords = await odoo.searchRecords( 'x_expense', [['id', '>', lastRecordId]], fields ); if (newRecords.length > 0) { await appendToCache(newRecords); updateLastRecordId(newRecords[newRecords.length - 1].id); } } ``` ### Optimistic Updates: ```javascript export async function create(data) { // 1. Generate temporary ID const tempId = `temp-${Date.now()}`; const tempRecord = { id: tempId, ...data }; // 2. Update UI immediately records.update(r => [...r, tempRecord]); // 3. Create in Odoo (background) try { const realId = await odoo.createRecord('x_expense', data); // 4. Replace temp ID with real ID records.update(r => r.map(rec => rec.id === tempId ? { ...rec, id: realId } : rec) ); } catch (error) { // 5. Rollback on error records.update(r => r.filter(rec => rec.id !== tempId)); throw error; } } ``` ### Background Sync Timer: ```javascript let syncInterval; export function startAutoSync() { syncInterval = setInterval(() => { syncInBackground(); }, 3 * 60 * 1000); // Every 3 minutes } export function stopAutoSync() { clearInterval(syncInterval); } ``` ### Partner Resolution Pattern: ```javascript // Many2one fields return [id, name] or just id // Resolve partner names and cache them async function resolvePartnerNames(records) { const partnerIds = new Set(); // Collect all unique partner IDs records.forEach(record => { if (record.x_studio_employee) { if (Array.isArray(record.x_studio_employee)) { partnerIds.add(record.x_studio_employee[0]); } else { partnerIds.add(record.x_studio_employee); } } }); // Fetch partner names in batch const partners = await odoo.fetchPartners(Array.from(partnerIds)); const partnerMap = new Map(partners.map(p => [p.id, p.name])); // Cache for future use localStorage.setItem('partnerCache', JSON.stringify( Array.from(partnerMap.entries()) )); return partnerMap; } ``` --- ## Layer 3: API Client (Odoo Communication) 🔌 ### Purpose: Abstract Odoo API communication with clean, reusable methods. ### JSON-RPC Communication: ```javascript async function jsonRpc(url, params) { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method: 'call', params: params, id: Math.random() }) }); const data = await response.json(); if (data.error) { throw new Error(data.error.message); } return data.result; } ``` ### CRUD Methods: #### Create ```javascript export async function createRecord(model, fields) { return await fetch('/api/odoo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'create', model, fields }) }); } ``` #### Read ```javascript export async function searchRecords(model, domain, fields) { return await fetch('/api/odoo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'search', model, domain, fields }) }); } ``` #### Update ```javascript export async function updateRecord(model, id, values) { return await fetch('/api/odoo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'update', model, id, values }) }); } ``` #### Delete ```javascript export async function deleteRecord(model, id) { return await fetch('/api/odoo', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete', model, id }) }); } ``` ### Field Formatting Helpers: ```javascript // Many2one: Convert to Odoo format [id, false] export function formatMany2one(id) { return id ? [id, false] : false; } // Many2many: Convert to Odoo command [(6, 0, [ids])] export function formatMany2many(ids) { return ids && ids.length > 0 ? [[6, 0, ids]] : [[6, 0, []]]; } ``` --- ## Layer 4: Server Routes (API Proxy) 🔐 ### Purpose: Securely handle Odoo authentication and proxy requests. ### Why Server-Side? 1. **Security** - API keys never exposed to client 2. **CORS** - Bypass cross-origin restrictions 3. **Caching** - Cache UIDs to reduce auth calls 4. **Error Handling** - Centralized error management ### UID Caching Pattern: ```javascript let cachedUid = null; async function authenticate() { if (cachedUid) { return cachedUid; } // Authenticate with Odoo cachedUid = await odooClient.authenticate( db, username, apiKey ); return cachedUid; } ``` ### Action Routing: ```javascript export async function POST({ request }) { const { action, model, ...params } = await request.json(); const uid = await authenticate(); switch (action) { case 'create': return odooClient.create(model, params.fields); case 'search': return odooClient.searchRead(model, params.domain, params.fields); case 'update': return odooClient.write(model, params.id, params.values); case 'delete': return odooClient.unlink(model, params.id); default: throw new Error(`Unknown action: ${action}`); } } ``` --- ## Data Flow Examples 🔄 ### Example 1: Loading Data on Page Load ``` 1. User navigates to page ↓ 2. Component calls expenseCache.load() ↓ 3. Cache store loads from localStorage/IndexedDB ↓ 4. UI updates with cached data (instant) ↓ 5. Cache checks if data is stale (> 5 min) ↓ 6. If stale, triggers background sync ↓ 7. API client calls /api/odoo ↓ 8. Server authenticates and calls Odoo ↓ 9. New records returned ↓ 10. Cache updated (localStorage + IndexedDB) ↓ 11. UI reactively updates with new data ``` ### Example 2: Creating a Record ``` 1. User submits form ↓ 2. Component calls expenseCache.create(data) ↓ 3. Cache creates temp record with temp-ID ↓ 4. UI updates immediately (optimistic) ↓ 5. API client calls /api/odoo (background) ↓ 6. Server creates record in Odoo ↓ 7. Real ID returned ↓ 8. Cache replaces temp-ID with real ID ↓ 9. localStorage and IndexedDB updated ↓ 10. UI shows success message ``` ### Example 3: Offline Create → Online Sync ``` 1. User creates record (offline) ↓ 2. Record saved to cache with temp-ID ↓ 3. API call fails (no network) ↓ 4. Record marked as "pending sync" ↓ 5. UI shows "Will sync when online" ↓ [User goes online] ↓ 6. Background sync detects pending records ↓ 7. Retries API call ↓ 8. Success! Real ID received ↓ 9. Cache updated ↓ 10. UI shows "Synced" status ``` --- ## Key Design Patterns 🎯 ### 1. Offline-First - Always load from cache first - Sync in background - Queue operations when offline - Retry on reconnection ### 2. Optimistic UI - Update UI immediately - Sync with server in background - Rollback on error - Show pending states ### 3. Incremental Sync - Don't re-fetch all data - Only fetch new records (id > lastRecordId) - Reduces bandwidth - Faster sync times ### 4. Dual Storage - localStorage for metadata (fast) - IndexedDB for data (large) - Best of both worlds ### 5. Partner Resolution - Batch fetch related records - Cache partner names - Avoid N+1 queries - Display human-readable names ### 6. Reactive State - Framework-native reactivity - UI updates automatically - No manual DOM manipulation - Cleaner code --- ## Performance Considerations ⚡ ### Initial Load: - Cache-first: Instant data display - Background sync: Fetch updates without blocking - IndexedDB: Fast access to large datasets ### Network Usage: - Incremental sync: Only new data - Batch operations: Combine requests - UID caching: Reduce auth calls ### Memory Usage: - Store large data in IndexedDB, not memory - Clean up old data periodically - Lazy load related data ### Bundle Size: - Framework-specific optimizations - Tree shaking - Code splitting - Lazy load routes --- ## Security Patterns 🔒 ### Credential Management: - API keys in environment variables - Server-side authentication - Never expose keys to client - Rotate keys periodically ### Data Validation: - Validate on client (UX) - Validate on server (security) - Sanitize inputs - Check permissions ### Error Handling: - Don't expose internal errors to user - Log errors securely - Graceful degradation - User-friendly messages --- ## Example prompts to use this command: - `/architecture` - Show architecture details - User: "How does the caching work?" - User: "Explain the data flow" - User: "What design patterns are used?" ## Next Steps: After understanding the architecture: 1. Review generated code in your project 2. Read CLAUDE.md in your project for specific details 3. Customize patterns for your use case 4. Optimize for your specific needs For more information, see `/help` or `/examples`!