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

2. Universe Metadata โ†’ MongoDB + Local Storage

3. Layout Changes Trigger Metadata Update


๐Ÿ”„ 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

2. Timestamps Trigger UI Refresh

3. Current Limitations

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

  1. Make layout change in Browser 1
  2. Click Save
  3. 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

  1. Change universe name in Backend/MongoDB
  2. Wait 10 seconds
  3. Browser should show updated name (not layout, just metadata)

Test 3: Verify Layout Persists Locally

  1. Add sections in Browser 1
  2. Refresh page
  3. 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)

Phase 2: Layout Sync API

Phase 3: Real-Time Collaboration


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