OFFLINE FIRST ARCHITECTURE PLAN
Documentation for Plans
Offline-First Architecture - Implementation Plan
🎯 Vision
storage-system with Flash Persistence is ALWAYS the source of truth for READ operations
MongoDB is the sync destination for PERSISTENCE
Performance is TOP priority - local-first = light speed ⚡
🚀 Innovative Architecture: Our groundbreaking storage-system with Flash Persistence provides:
- ⚡ Intelligent Storage: Multi-adapter (OPFS, IndexedDB, localStorage) with auto-migration
- 🚀 Flash Persistence: Zero-config state management with reactive signals (NGXS replacement)
- 🧠 AI-Powered: Pattern learning and predictive caching
- 📦 Auto-Discovery: Runtime state discovery and on-demand loading
- 🔧 Storage Registry: Intelligent backend selection
🔴 Current Architecture (BROKEN)
How It Works Now:
// ❌ CURRENT FLOW (Inconsistent)
if (useMockApi) {
// Mock Mode: 100% storage-system, NO backend
read → storage-system (Flash Persistence)
write → storage-system (Flash Persistence)
sync → NEVER
} else {
// Real Mode: Backend-first (SLOW!)
read → MongoDB API → storage-system cache
write → MongoDB API → storage-system cache
sync → Immediate, but inconsistent
}
Problems:
- Two completely different code paths
- Mock mode data ≠ Real mode data
- Switching modes = data loss
- Backend calls on EVERY operation (slow!)
- No offline tolerance in "Real" mode
- No conflict resolution
- Orphaned data everywhere
✅ Target Architecture (OFFLINE-FIRST)
The Right Way:
// ✅ OFFLINE-FIRST FLOW (Consistent) - Innovative storage-system Architecture
// ALWAYS storage-system (Flash Persistence) first, ALWAYS fast, ALWAYS works offline
// READ (ALWAYS fast - local first via storage-system)
read → storage-system (Flash Persistence) (instant! 5-10ms)
↓
🧠 Intelligent Storage selects optimal backend (OPFS/IndexedDB/localStorage)
🚀 Flash Persistence auto-discovers and loads states
↓
if (stale || missing) → fetch from MongoDB in background
↓
update storage-system for next read
// WRITE (ALWAYS fast - local first via storage-system)
write → storage-system (Flash Persistence) (instant! 5-10ms)
↓
🚀 Flash Persistence automatically persists via autoFlashState()
🧠 AI-powered pattern learning and predictive caching
↓
queue for sync (non-blocking)
↓
sync to MongoDB when online (background)
// SYNC (Background, non-blocking)
if (syncEnabled && isOnline) {
background: compare storage-system ↔ MongoDB
resolve conflicts (local wins by default)
push local changes → MongoDB
pull remote changes → storage-system
}
Key Principles:
- storage-system (with Flash Persistence) is ALWAYS the source of truth for reads ⚡
- Intelligent Storage: Multi-adapter with auto-migration
- Flash Persistence: Zero-config reactive state management
- All writes go to storage-system FIRST (instant 5-10ms)
- Flash Persistence auto-discovers and persists
- AI-powered optimization and caching
- Background sync to MongoDB (non-blocking)
- Mock mode = sync disabled (100% local via storage-system)
- Real mode = sync enabled (storage-system + MongoDB sync)
- Works offline in BOTH modes (thanks to storage-system)
- Switching modes = just toggle sync flag (seamless data preservation)
🏗️ Implementation Strategy
Phase 1: Unified Storage Layer ✅ (Already exists!)
Good News: You already have @storage-system with:
- ✅ IndexedDB adapter
- ✅ Intelligent storage
- ✅ Policy engine
- ✅ Offline-first capabilities
But: Not properly integrated with backend sync!
Phase 2: Sync Engine (NEEDS IMPLEMENTATION)
Create a proper Sync Service that handles:
// apps/gograb-space/src/app/services/sync-engine.service.ts
@Injectable({ providedIn: 'root' })
export class SyncEngineService {
// Configuration
private syncEnabled = signal(false); // Toggle with useMockApi
private syncInterval = 5000; // 5 seconds
private syncInProgress = signal(false);
// Sync queue (operations to push to backend)
private syncQueue = autoFlashState<SyncOperation[]>('sync-queue', []);
// Last sync timestamps (for each entity type)
private lastSync = autoFlashState<Record<string, number>>('last-sync', {});
/**
* Initialize sync engine
* Called at app startup
*/
async initialize(config: { syncEnabled: boolean }): Promise<void> {
this.syncEnabled.set(config.syncEnabled);
if (config.syncEnabled) {
// Start background sync
this.startBackgroundSync();
}
}
/**
* Queue a write operation for sync
* ALWAYS writes to IndexedDB first!
*/
async write<T>(key: string, data: T, entity: 'space' | 'universe' | 'page'): Promise<void> {
// 1. Write to IndexedDB FIRST (instant!)
await this.storage.put(key, data);
// 2. Queue for backend sync (non-blocking)
if (this.syncEnabled()) {
this.syncQueue.update(queue => [...queue, {
operation: 'write',
entity,
key,
data,
timestamp: Date.now(),
}]);
}
// Done! User sees instant response ⚡
}
/**
* Read operation - ALWAYS from IndexedDB
*/
async read<T>(key: string): Promise<T | null> {
// Always read from IndexedDB (instant!)
const data = await this.storage.get<T>(key);
// If stale or missing, fetch in background (don't block!)
if (this.syncEnabled() && this.isStale(key)) {
this.fetchFromBackendInBackground(key);
}
return data;
}
/**
* Background sync - runs periodically
*/
private async startBackgroundSync(): Promise<void> {
setInterval(() => {
if (!this.syncInProgress() && this.syncEnabled() && navigator.onLine) {
this.performSync();
}
}, this.syncInterval);
}
/**
* Perform bidirectional sync
*/
private async performSync(): Promise<void> {
this.syncInProgress.set(true);
try {
// 1. Push local changes to backend (from sync queue)
await this.pushToBackend();
// 2. Pull remote changes from backend
await this.pullFromBackend();
// 3. Update last sync timestamp
this.lastSync.update(times => ({
...times,
lastFullSync: Date.now()
}));
} catch (error) {
console.error('[SyncEngine] Sync failed:', error);
} finally {
this.syncInProgress.set(false);
}
}
/**
* Push queued operations to backend
*/
private async pushToBackend(): Promise<void> {
const queue = this.syncQueue();
if (queue.length === 0) return;
for (const op of queue) {
try {
// Send to backend API
await this.backendApi.sync(op);
// Remove from queue
this.syncQueue.update(q => q.filter(item => item !== op));
} catch (error) {
console.error('[SyncEngine] Failed to sync operation:', op, error);
// Keep in queue for retry
}
}
}
/**
* Pull changes from backend
*/
private async pullFromBackend(): Promise<void> {
const lastSyncTime = this.lastSync().lastFullSync || 0;
// Fetch changes since last sync
const changes = await this.backendApi.getChangesSince(lastSyncTime);
// Apply changes to IndexedDB
for (const change of changes) {
// Conflict resolution: Check if local version is newer
const localData = await this.storage.get(change.key);
const localTimestamp = localData?.updatedAt || 0;
const remoteTimestamp = change.data.updatedAt || 0;
if (remoteTimestamp > localTimestamp) {
// Remote is newer, update local
await this.storage.put(change.key, change.data);
} else {
// Local is newer or same, keep local (local wins!)
console.log('[SyncEngine] Local version is newer, keeping local');
}
}
}
/**
* Enable/disable sync (for mode switching)
*/
setSyncEnabled(enabled: boolean): void {
this.syncEnabled.set(enabled);
if (enabled) {
this.startBackgroundSync();
// Immediate sync when enabled
this.performSync();
}
}
}
Phase 3: Update SpaceService to Use Sync Engine
// libs/core-angular/src/lib/services/space.service.ts
@Injectable({ providedIn: 'root' })
export class SpaceService {
private syncEngine = inject(SyncEngineService);
/**
* Create universe - ALWAYS fast!
*/
async createUniverse(data: UniverseCreate): Promise<Universe> {
const universe: Universe = {
id: `universe_${Date.now()}`,
...data,
createdAt: new Date().toISOString(),
};
// Write to IndexedDB FIRST (instant!)
await this.syncEngine.write(`universe_${universe.id}`, universe, 'universe');
// Update space's universeIds
const space = this._currentSpace();
space.universeIds.push(universe.id);
await this.syncEngine.write(`space_${space.id}`, space, 'space');
// Update signals
this._universes.update(u => [...u, universe]);
// Done! Sync happens in background
return universe;
}
/**
* Load universes - ALWAYS fast!
*/
async loadUniverses(spaceId: string): Promise<Universe[]> {
const space = await this.syncEngine.read<Space>(`space_${spaceId}`);
if (!space) return [];
// Load all universes from IndexedDB (instant!)
const universes = await Promise.all(
space.universeIds.map(id => this.syncEngine.read<Universe>(`universe_${id}`))
);
return universes.filter(u => u !== null) as Universe[];
}
}
Phase 4: Update Environment Configuration
// apps/gograb-space/src/main.ts
// Initialize sync engine based on environment
const syncEngine = inject(SyncEngineService);
await syncEngine.initialize({
syncEnabled: !environment.useMockApi, // Mock mode = sync OFF
backendUrl: environment.backendApiUrl,
syncInterval: 5000, // 5 seconds
conflictResolution: 'local-wins', // Always prefer local changes
});
console.log(`🔄 Sync Engine: ${environment.useMockApi ? 'DISABLED (Mock Mode)' : 'ENABLED (Real Backend)'}`);
📊 Benefits
Before (Current):
Create Universe:
→ API call to MongoDB (500ms) ❌ SLOW
→ Cache in IndexedDB
→ User sees result (500ms later)
Switch Mock → Real:
→ Data loss ❌
→ Need nuclear reset ❌
After (Offline-First):
Create Universe:
→ Write to IndexedDB (5ms) ✅ INSTANT
→ Queue for sync (non-blocking)
→ User sees result immediately ⚡
→ Sync to MongoDB in background
Switch Mock → Real:
→ Just enable sync ✅
→ Data preserved ✅
→ Background sync starts ✅
🎯 Performance Impact
| Operation | Before (Backend-First) | After (Offline-First) | Improvement |
|---|---|---|---|
| Read Space | 200-500ms (API) | 5-10ms (IndexedDB) | 50x faster |
| Create Universe | 300-600ms (API) | 5-10ms (IndexedDB) | 60x faster |
| Load Dashboard | 1-2s (multiple APIs) | 50-100ms (local) | 20x faster |
| Offline Use | ❌ Breaks | ✅ Works | Infinite |
| Mode Switch | ❌ Data loss | ✅ Seamless | Perfect |
🚀 Implementation Phases
Phase 1: Foundation (1-2 days)
- ✅ Review existing storage-system
- ✅ Design sync engine architecture
- ✅ Create SyncEngineService skeleton
- ✅ Add sync queue with autoFlashState
- ✅ Implement basic push/pull logic
Phase 2: Integration (2-3 days)
- Update SpaceService to use sync engine
- Update all data services (universes, pages, etc.)
- Test offline → online transitions
- Test mock → real mode switching
Phase 3: Conflict Resolution (1-2 days)
- Implement "local wins" strategy
- Add timestamp-based conflict detection
- Test multi-device scenarios
- Handle edge cases
Phase 4: Optimization (1-2 days)
- Batch sync operations
- Implement delta sync (only changed data)
- Add retry logic with exponential backoff
- Performance testing
Phase 5: Polish (1 day)
- Add sync status indicators in UI
- Add manual sync trigger
- Documentation
- E2E testing
🔧 Configuration Options
// apps/gograb-space/src/environments/environment.ts
export const environment = {
// ... other config
sync: {
enabled: !useMockApi, // Auto-configure based on mode
interval: 5000, // 5 seconds
retryAttempts: 3,
retryDelay: 1000, // 1 second
conflictResolution: 'local-wins', // or 'remote-wins', 'manual'
batchSize: 10, // Sync 10 operations at a time
deltaSync: true, // Only sync changed data
}
};
📝 Migration Plan
For Existing Users:
- Detect old data format
- One-time migration: Copy IndexedDB → MongoDB
- Enable sync
- Clean up duplicates
For New Users:
- Create in IndexedDB (instant)
- Sync to MongoDB (background)
- Everything just works ✅
🎯 Success Metrics
- Read Performance: < 10ms (from IndexedDB)
- Write Performance: < 10ms (to IndexedDB)
- Sync Latency: < 5s (background)
- Offline Tolerance: 100% (works completely offline)
- Mode Switch: Seamless (no data loss)
- User Experience: ⚡ LIGHT SPEED
📚 References
Similar architectures:
- PouchDB - CouchDB sync protocol
- RxDB - Realtime database with sync
- WatermelonDB - Offline-first for React Native
- Dexie Cloud - Dexie.js sync layer
Status: 🔴 PLANNING
Priority: 🔥 CRITICAL
Impact: 🚀 MASSIVE (50-60x performance improvement)
Effort: 📅 7-10 days
Dependencies: Existing storage-system foundation
This is the right architecture. Let's build it properly! 🎯