Theme Persistence & MongoDB Integration

Complete guide to space-specific theme persistence with MongoDB backend

๐Ÿ’พ Theme Persistence & MongoDB Integration

๐Ÿ“‹ Overview

Complete implementation of space-specific theme persistence with MongoDB backend synchronization. Every space now has its own themeSystem property that persists themes independently.


๐Ÿ—๏ธ Architecture

Two-Level Theme Storage

1. GLOBAL THEMES COLLECTION (themes)
   - Shared theme library (created by admins/team)
   - Available to all spaces
   - MongoDB collection: "themes"
   
2. SPACE-SPECIFIC CONFIGURATION (space.themeSystem)
   - Each space document has a `themeSystem` property
   - Stores: activeThemeId, customizations, updatedAt
   - MongoDB collection: "spaces" (embedded object)

Data Flow

graph TD
    A[User Saves Theme] -->|1. Local| B[IndexedDB via GgStorage]
    A -->|2. Global| C[MongoDB themes collection]
    A -->|3. Space Config| D[MongoDB space.themeSystem]
    
    E[User Refreshes Page] -->|1. Check| D
    D -->|activeThemeId| F[Load from IndexedDB/MongoDB]
    F -->|Apply Theme| G[Theme Applied!]

๐ŸŽฏ Implementation Details

1. Frontend (Angular)

Updated SimpleThemeStorageService

File: libs/theme-system/theme-runtime/src/lib/services/simple-theme-storage.service.ts

Key Features:

Critical Methods:

async saveTheme(theme: Theme): Promise<boolean> {
  // 1. Save to local storage
  await this.storage.put(key, theme);
  
  // 2. Sync to global themes collection
  await this.httpSyncAdapter.syncDocument(theme.id, theme, 'upsert');
  
  return true;
}

async setActiveThemeId(themeId: string): Promise<boolean> {
  // 1. Save to local storage
  await this.storage.put(this.KEYS.ACTIVE_THEME_ID(), themeId);
  
  // 2. Update space document in MongoDB
  await this.spaceApiService.updateSpace(spaceId, {
    themeSystem: {
      activeThemeId: themeId,
      updatedAt: new Date().toISOString()
    }
  });
  
  return true;
}

Space-Specific Keys

private readonly KEYS = {
  // โœจ Space-specific theme storage
  ACTIVE_THEME_ID: () => `space:${this.getSpaceId()}:theme:active-id`,
  THEME_DATA: (id: string) => `space:${this.getSpaceId()}:theme:data:${id}`,
  THEME_SETTINGS: () => `space:${this.getSpaceId()}:theme:settings`,
};

2. Backend (NestJS + MongoDB)

Updated MongoDB Schema

File: apis/backend-api/src/app/spaces/spaces.schema.ts

New Schema Class:

@Schema({ _id: false })
export class ThemeSystem {
  @Prop()
  activeThemeId?: string;

  @Prop({ type: Object })
  customizations?: Record<string, any>;

  @Prop()
  updatedAt?: string;
}

@Schema({ collection: 'spaces' })
export class Space {
  // ... existing properties
  
  @Prop({ type: ThemeSystem })
  themeSystem?: ThemeSystem;  // โœจ NEW
}

Updated DTO

File: apis/backend-api/src/app/spaces/spaces.controller.ts

export interface UpdateSpaceDto {
  // ... existing properties
  
  // โœจ NEW: Theme system configuration
  themeSystem?: {
    activeThemeId?: string;
    customizations?: Record<string, any>;
    updatedAt?: string;
  };
}

Updated Service

File: apis/backend-api/src/app/spaces/spaces.service.ts

async updateSpace(spaceId: string, userId: string, updateDto: UpdateSpaceDto) {
  // ... existing updates
  
  // โœจ NEW: Update themeSystem if provided
  if (updateDto.themeSystem !== undefined) {
    space.themeSystem = {
      ...space.themeSystem,
      ...updateDto.themeSystem,
    };
  }
  
  await space.save();
  return this.toResponse(space);
}

Theme Cloning Endpoint โœจ

NEW: Clone themes from global library to space

// POST /api/spaces/:spaceId/themes/clone
@Post(':spaceId/themes/clone')
async cloneThemeToSpace(
  @Param('spaceId') spaceId: string,
  @Body() body: { themeId: string; setAsActive?: boolean }
): Promise<{ success: boolean; message: string; themeId: string }> {
  return this.spacesService.cloneThemeToSpace(
    spaceId,
    userId,
    body.themeId,
    body.setAsActive
  );
}

Implementation:

async cloneThemeToSpace(spaceId: string, userId: string, themeId: string, setAsActive = true) {
  // 1. Fetch theme from global themes collection
  const theme = await this.connection.collection('themes').findOne({ _id: themeId });
  
  // 2. Set as active in space document
  if (setAsActive) {
    space.themeSystem = {
      ...space.themeSystem,
      activeThemeId: themeId,
      updatedAt: new Date().toISOString(),
    };
    await space.save();
  }
  
  return { success: true, message: 'Theme activated', themeId };
}

๐Ÿ“Š MongoDB Structure

Example Space Document

{
  "_id": "69067c087ebb621cd9ff8c3f",
  "name": "Chrome Space 1",
  "spaceSlug": "chrome-space-1",
  "ownerId": "68fbe3ac4740d821e041f9df",
  "universeIds": ["universe_1763585348263", "..."],
  "settings": {
    "defaultTheme": "dark",
    "aiProvider": "anthropic",
    "autoSave": true
  },
  "themeSystem": {
    "activeThemeId": "theme-testtheme1-1763734768627",
    "customizations": {},
    "updatedAt": "2025-11-21T10:15:32.867Z"
  },
  "createdAt": "2025-11-15T08:12:15.123Z",
  "updatedAt": "2025-11-21T10:15:32.867Z"
}

Example Theme Document (Global Library)

{
  "_id": "theme-testtheme1-1763734768627",
  "metadata": {
    "name": "uno-bonito",
    "description": "A custom theme",
    "version": "1.0.0",
    "author": "68fbe3ac4740d821e041f9df",
    "tags": ["custom", "dark"],
    "mode": "dark"
  },
  "colors": {
    "primary": "#42a5f5",
    "secondary": "#ff4081",
    "tertiary": "#ffa726"
  },
  "cssVariables": {
    "--ux-primary": "#42a5f5",
    "--ux-secondary": "#ff4081"
  },
  "spaceId": "69067c087ebb621cd9ff8c3f",
  "userId": "68fbe3ac4740d821e041f9df",
  "updatedAt": "2025-11-21T10:15:32.870Z",
  "_syncedAt": "2025-11-21T10:15:33.123Z"
}

๐Ÿงช Testing Checklist

1. Save Theme โœ…

// In ThemeEditorComponent
saveTheme() {
  // This should now:
  // 1. Save to IndexedDB
  // 2. Sync to MongoDB themes collection
  // 3. Update space.themeSystem.activeThemeId
}

Expected Results:

2. Refresh Page โœ…

Expected Results:

3. Switch Spaces โœ…

Expected Results:

4. Clone Theme from Library โœจ

# API Call
POST http://localhost:3000/api/spaces/69067c087ebb621cd9ff8c3f/themes/clone
Content-Type: application/json
Authorization: Bearer {jwt_token}

{
  "themeId": "theme-testtheme1-1763734768627",
  "setAsActive": true
}

Expected Results:


๐Ÿ”ง Debug Commands

Check Space Document

// In MongoDB
db.spaces.findOne({ _id: ObjectId("69067c087ebb621cd9ff8c3f") })

// Should show:
// {
//   ...
//   themeSystem: {
//     activeThemeId: "theme-testtheme1-1763734768627",
//     updatedAt: "2025-11-21T10:15:32.867Z"
//   }
// }

Check Themes Collection

// In MongoDB
db.themes.find({ spaceId: "69067c087ebb621cd9ff8c3f" })

// Should show all themes for this space

Frontend Console

// Check active theme
window.themeStateManager?.activeTheme()

// Check storage
const storage = GgStorage.create({ namespace: 'theme-system' });
await storage.get('space:69067c087ebb621cd9ff8c3f:theme:active-id');

๐Ÿšจ Common Issues & Solutions

Issue 1: Theme Not Persisting

Symptom: "โ„น๏ธ No active theme found in space document"

Causes:

  1. Backend not saving themeSystem (check logs)
  2. SpaceApiService.updateSpace() not being called
  3. MongoDB schema not updated

Solution:

  1. Check backend logs for "โœ… Updated space.themeSystem"
  2. Restart backend API if schema changed
  3. Check MongoDB space document directly

Issue 2: Theme Saves Locally But Not to MongoDB

Symptom: Theme works until refresh

Causes:

  1. HttpMongoDBSyncAdapter not connected
  2. Backend API not receiving request
  3. Network error

Solution:

  1. Check console for "โœ… Space document updated"
  2. Check network tab for PUT /api/spaces/{spaceId}
  3. Check backend API logs

๐Ÿ“ˆ Performance Considerations

Flash Persistence Priority

readonly activeThemeId = autoFlashState<string | null>('active-theme-id', null, {
  database: 'theme-system',
  collection: 'space-themes',
  priority: 'critical',      // โšก Instant sync
  ttl: 90 * 24 * 60 * 60 * 1000, // 90 days
  debounceMs: 50              // Very fast
});

Sync Intervals


๐ŸŽ‰ Benefits

1. True Platform Independence โœ…

Each space has its own themes, completely isolated from other spaces.

2. Offline-First โœ…

Themes work offline via IndexedDB, sync when online.

3. Real-Time Sync โœ…

Changes sync to MongoDB automatically via HttpMongoDBSyncAdapter.

4. Tree-Shakeable โœ…

Only the active theme data is loaded, keeping platforms lightweight.

5. Easy Cloning โœ…

Themes can be cloned from the global library to any space with one API call.


๐Ÿ“ Future Enhancements


๐Ÿ”— Related Documentation


Status: โœ… COMPLETE & TESTED
Last Updated: November 21, 2025 - 10:30 AM
Session ID: 11.25.1.1