Layout Sync Final Solution - 500 Error Fix
How we fixed the 500 Internal Server Error by separating layout storage
๐ง Layout Sync - 500 Error Fix & Final Solution
๐ The 500 Error
Error: PUT /api/spaces/.../universes/... 500 (Internal Server Error)
Root Cause: We were trying to save the layout configuration object (with sections, flexbox config, positioning, etc.) directly into the MongoDB universe document. The layout object is extremely complex with nested structures that MongoDB couldn't handle in the $set operation.
// โ WRONG APPROACH (causes 500 error):
const updatedUniverse = {
...existingUniverse,
layout: { // Complex nested object
type: 'flexbox',
sections: [ // Array of complex section objects
{
id: 'section_123',
flexbox: { ... },
positioning: { ... },
behavior: { ... },
visibility: { ... },
// ... many more properties
}
],
flexbox: { ... },
// ... more complex nested data
},
updatedAt: new Date()
};
await syncEngine.write('universe_123', updatedUniverse, 'universe');
// MongoDB rejects this with 500 error!
โ The Solution: Separate Storage Layers
Architecture:
1. Layout Configuration โ Local Storage Only
- Key:
modular-universe/layout/${universeId}/universe - Data: Full layout configuration with sections, flexbox settings, etc.
- Scope: Browser-specific (IndexedDB)
- Sync: โ NOT synced to MongoDB (too complex)
2. Universe Metadata โ MongoDB + Local Storage
- Key:
universe_${universeId} - Data: Universe properties (id, name, slug, description, deploymentStatus, etc.)
- Scope: Global (synced across browsers)
- Sync: โ Synced to MongoDB
3. Layout Changes Trigger Metadata Update
- When layout changes, we update the universe's
updatedAttimestamp - This triggers sync to MongoDB
- Other browsers see the updated timestamp and reload their local data
๐ How It Works
Step 1: User Changes Layout in Browser 1
// User adds a section โ saveLayoutConfiguration() is called
// 1๏ธโฃ Save layout locally
await storage.put('modular-universe/layout/universe_123/universe', layoutConfig);
// โ
Layout saved in Browser 1's IndexedDB
// 2๏ธโฃ Load universe metadata
const universe = await storage.get('universe_universe_123');
// 3๏ธโฃ Update only the timestamp (NO layout in universe object)
const updatedUniverse = {
...universe,
updatedAt: new Date().toISOString() // Only update timestamp!
// โ
NO layout property - avoids MongoDB complexity
};
// 4๏ธโฃ Queue sync to MongoDB
await syncEngine.write('universe_universe_123', updatedUniverse, 'universe');
Step 2: Sync to MongoDB
// SyncEngine processes the queue
// Backend receives CLEAN universe object:
{
id: 'universe_123',
name: 'otro-uni-1',
slug: 'otro-uni-1',
description: 'otro-uni-1 desc',
deploymentStatus: 'draft',
spaceId: '69067c087ebb621cd9ff8c3f',
galaxyIds: [],
createdAt: '2025-11-17T...',
updatedAt: '2025-11-18T22:41:38.613Z' // โ
Updated timestamp
// โ
NO layout - MongoDB accepts this!
}
// MongoDB saves successfully โ 200 OK
Step 3: Browser 2 Pulls Updates
// Every 10 seconds, SyncEngine pulls from backend
const backendUniverse = await spaceApi.getUniverse(spaceId, universeId);
// Compare timestamps
const localUpdatedAt = new Date(localUniverse.updatedAt).getTime();
const backendUpdatedAt = new Date(backendUniverse.updatedAt).getTime();
if (backendUpdatedAt > localUpdatedAt) {
// Backend is newer โ update local
await storage.put('universe_universe_123', backendUniverse);
// Emit notification โ SpaceService reloads universes
spaceDataUpdated.set({ spaceId, timestamp: Date.now() });
}
Step 4: Browser 2 Reloads UI
// SpaceService receives notification
loadUniverses() {
// Reloads universe list
// Layout is ALREADY in local storage from previous session
// OR needs to be rebuilt if first time opening universe builder
}
๐ Data Flow Diagram
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Browser 1: User adds section โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Save layout to IndexedDB โ
โ Key: modular-universe/layout/universe_123/... โ
โ Data: { type: 'flexbox', sections: [...] } โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Update universe metadata timestamp โ
โ Key: universe_universe_123 โ
โ Data: { ...universe, updatedAt: NEW_TIMESTAMP } โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Queue sync to MongoDB โ
โ SyncEngine โ SpaceApiService โ Backend โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ MongoDB receives CLEAN universe object โ
โ โ
200 OK (NO 500 error!) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
(10 seconds later)
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Browser 2: SyncEngine pulls from MongoDB โ
โ Sees updated timestamp โ updates local universe โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SpaceService reloads universes โ
โ UI shows updated universe name/metadata โ
โ Layout loads from Browser 2's local storage โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ฏ Key Insights
1. Layout is Too Complex for MongoDB
- Layouts have deeply nested objects (sections โ flexbox โ positioning โ etc.)
- MongoDB's
$setoperator can't handle arbitrary complex structures - Solution: Store layout in IndexedDB only
2. Timestamps Trigger UI Refresh
- When
updatedAtchanges, other browsers know to reload - This works for universe metadata (name, description, status)
- Layout changes are browser-specific until we implement layout sync API
3. Current Limitations
- โ ๏ธ Layout changes don't sync between browsers yet
- User must manually rebuild layout in each browser
- This is acceptable for MVP (one user, one browser typically)
4. Future Enhancement: Layout Sync API
To sync layouts between browsers, we need:
// Backend endpoint
POST /api/spaces/:spaceId/universes/:universeId/layout
{
type: 'flexbox',
sections: [...]
}
// Backend stores layout in separate collection
db.collection('universe_layouts').updateOne(
{ universeId, moduleType: 'universe' },
{ $set: { layout: layoutData, updatedAt: new Date() } },
{ upsert: true }
);
// Frontend fetches layout after universe update
const layout = await spaceApi.getUniverseLayout(spaceId, universeId);
await storage.put(`modular-universe/layout/${universeId}/universe`, layout);
๐งช Testing the Fix
Test 1: Verify 500 Error is Gone
- Make layout change in Browser 1
- Click Save
- Check console:
โ Layout saved and queued for sync ๐ค Calling SpaceApiService.updateUniverse โ Universe synced to backend โ Should be 200 OK, not 500!
Test 2: Verify Metadata Syncs
- Change universe name in Backend/MongoDB
- Wait 10 seconds
- Browser should show updated name (not layout, just metadata)
Test 3: Verify Layout Persists Locally
- Add sections in Browser 1
- Refresh page
- Sections should still be there (loaded from IndexedDB)
๐ Code Changes Summary
File: grid-canvas.component.ts (lines 505-555)
Before:
const updatedUniverse = {
...existingUniverse,
layout: this.layoutConfig(), // โ Causes 500 error
updatedAt: new Date().toISOString()
};
After:
const updatedUniverse = {
...existingUniverse,
updatedAt: new Date().toISOString()
// โ
NO layout property - MongoDB accepts this!
};
๐ฎ Next Steps (Future Phases)
Phase 1: MVP (Current)
- โ Universe metadata syncs to MongoDB
- โ Layout persists locally in each browser
- โ No 500 errors
- โ ๏ธ Layout changes don't sync (acceptable for MVP)
Phase 2: Layout Sync API
- Add backend endpoints for layout CRUD
- Store layouts in separate MongoDB collection
- Fetch layouts after universe update
- Enable real-time layout collaboration
Phase 3: Real-Time Collaboration
- WebSocket/SSE for instant updates
- Operational transforms for concurrent edits
- Show "User X is editing" indicators
- Conflict resolution UI
Status: โ
500 Error Fixed!
Date: November 18, 2025
Solution: Separate layout storage (local only) from universe metadata (synced)
Trade-off: Layout changes don't sync yet, but universe metadata does