Skip to content

Unified Viewer Architecture

This document describes the unified viewer architecture that allows the same viewer bundle to power both: 1. Hosted Mode - Tours on xrgallery.online with Firebase integration 2. Export Mode - Self-hosted standalone packages without Firebase

Overview

The viewer uses runtime configuration injection rather than build-time configuration. This means: - One codebase, one build process - Firebase credentials are injected at runtime (only XRGallery can provide valid credentials) - Exports get the full visual experience but cannot access Firebase services

┌─────────────────────────────────────────────────────────────────────┐
│                        Unified Viewer Bundle                        │
│                         (viewer-bundle.iife.js)                     │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌─────────────────────┐     ┌─────────────────────┐               │
│  │   ServiceFactory    │────▶│   ViewerServices    │               │
│  └─────────────────────┘     └─────────────────────┘               │
│           │                            │                            │
│           ▼                            ▼                            │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │                                                             │   │
│  │  Firebase Config?  ──YES──▶  FirebaseConfigProvider         │   │
│  │        │                     FirebaseAnalyticsService       │   │
│  │        │                     FirebaseUserService            │   │
│  │        │                                                    │   │
│  │       NO                                                    │   │
│  │        │                                                    │   │
│  │        ▼                                                    │   │
│  │  Embedded Config?  ──YES──▶  EmbeddedConfigProvider         │   │
│  │                              NoopAnalyticsService           │   │
│  │                              StubUserService                │   │
│  │                                                             │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

Service Architecture

Interfaces (src/services/interfaces.ts)

The core interfaces define contracts for services:

interface IConfigProvider {
  loadConfig(): Promise<LoadedConfig>;
  requiresPassword(): Promise<boolean>;
  verifyPassword(password: string): Promise<boolean>;
  getSequenceId(): string | null;
}

interface IAnalyticsService {
  setContext(sequenceId: string | null, userId: string | null): void;
  trackSessionStarted(): void;
  trackHotspotClicked(hotspotId: string, hotspotType: string): void;
  // ... other tracking methods
  dispose(): void;
}

interface IUserProfileService {
  getUserProfile(userId: string): Promise<UserProfileData | null>;
  hasFeatureAccess(profile: UserProfileData, feature: FeatureKey): boolean;
}

Implementations

Firebase Services (src/services/firebase/)

  • FirebaseConfigProvider - Fetches config from Firestore
  • FirebaseAnalyticsService - Wraps existing Analytics service
  • FirebaseUserService - Wraps UserProfileAPI

Stub Services (src/services/stub/)

  • EmbeddedConfigProvider - Returns config from window.__XRGALLERY_CONFIG__
  • NoopAnalyticsService - All tracking methods are no-ops
  • StubUserService - Returns true for all feature access

Service Factory (src/services/ServiceFactory.ts)

function createServices(options: InitOptions): ViewerServices {
  if (options.firebaseConfig) {
    // HOSTED MODE: Initialize Firebase, use Firebase services
    const app = initializeApp(options.firebaseConfig);
    return {
      configProvider: new FirebaseConfigProvider(app, options.sequenceId),
      analyticsService: new FirebaseAnalyticsService(app),
      userService: new FirebaseUserService(app),
    };
  } else if (options.config) {
    // EXPORT MODE: Use stub services with embedded config
    return {
      configProvider: new EmbeddedConfigProvider(options.config),
      analyticsService: new NoopAnalyticsService(),
      userService: new StubUserService(),
    };
  }
}

Initialization

Hosted Mode (viewer.html on xrgallery.online)

<script>
  window.__XRGALLERY_OPTIONS__ = {
    firebaseConfig: {
      apiKey: "...",
      authDomain: "...",
      projectId: "...",
      // etc.
    }
  };
</script>
<script type="module" src="/src/main.ts"></script>

The viewer detects window.__XRGALLERY_OPTIONS__ and initializes with Firebase services.

Export Mode (standalone HTML)

<script>
  window.__XRGALLERY_CONFIG__ = {
    experience: { title: "My Tour" },
    stages: [...]
  };
</script>
<script type="module">
  // Bundled viewer code (viewer-bundle.iife.js contents)
</script>

The viewer detects window.__XRGALLERY_CONFIG__ and initializes with stub services.

Build Process

Standard Build

npm run build

Builds the multi-page application (index.html, viewer.html, dashboard/, etc.)

Viewer Bundle Build

npm run build:viewer

Creates dist/viewer-bundle.iife.js - a self-contained IIFE bundle that can be embedded in exported HTML files.

Full Build

npm run build:all

Runs both builds sequentially.

Export Process (src/export/TourExporter.ts)

When a user exports a tour:

  1. Fetch Bundle: Get viewer-bundle.iife.js from /dist/
  2. Extract Assets: Get CSS and HTML body from viewer.html
  3. Generate HTML: Create standalone HTML with:
  4. Extracted CSS in <style> tag
  5. Viewer HTML body
  6. Embedded config in window.__XRGALLERY_CONFIG__
  7. Bundled viewer code in <script> tag
  8. Create ZIP: Package with config.json backup and README.md
async function exportTourPackage(sequence: Sequence): Promise<Blob> {
  const zip = new JSZip();

  const html = await generateExportHTML(sequence.config, options);
  zip.file('index.html', html);

  zip.file('config.json', JSON.stringify(sequence.config, null, 2));
  zip.file('README.md', generateReadme(sequence, options));

  return zip.generateAsync({ type: 'blob', compression: 'DEFLATE' });
}

Security Model

Why Exports Can't Access Firebase

  1. No Credentials: Exported packages don't include Firebase config
  2. Runtime Detection: Services are chosen based on what's available at runtime
  3. API Keys on Server: Firebase credentials only exist on xrgallery.online

What Exports Get

  • Full visual experience (liquid glass UI, portal animations, etc.)
  • All hotspot types (info, audio, navigation)
  • WebXR/VR support
  • Video streaming (HLS)
  • 3D model loading

What Exports Don't Get

  • Analytics tracking (NoopAnalyticsService)
  • Password protection verification
  • User profiles/tiers
  • Real-time config updates

File Structure

src/services/
├── interfaces.ts           # Core service interfaces
├── ServiceFactory.ts       # Creates appropriate service implementations
├── firebase/
│   ├── FirebaseConfigProvider.ts
│   ├── FirebaseAnalyticsService.ts
│   └── FirebaseUserService.ts
└── stub/
    ├── EmbeddedConfigProvider.ts
    ├── NoopAnalyticsService.ts
    └── StubUserService.ts

src/export/
└── TourExporter.ts         # Generates export packages

dist/
└── viewer-bundle.iife.js   # Self-contained viewer bundle (after build:viewer)

Development Workflow

Testing Export Mode Locally

  1. Build the viewer bundle:

    npm run build:viewer
    

  2. Run the dev server:

    npm run dev
    

  3. Use the export feature from the dashboard

Testing Hosted Mode

Just run:

npm run dev

The dev server uses viewer.html which includes Firebase credentials.

Bundle Size

The viewer bundle includes: - Babylon.js engine (~3MB gzipped) - HLS.js (~50KB gzipped) - All UI components (liquid glass, portals, etc.) - Service implementations

Total: ~3.1 MB gzipped

This is acceptable for export packages since: - Users download once then have offline access - The bundle is fully self-contained - No additional network requests for code

Future Improvements

  1. Code Splitting: Could reduce hosted viewer load time by lazy-loading features
  2. CDN for Exports: Option to load viewer from CDN instead of embedding
  3. Partial Analytics: Basic anonymous analytics for exports via different service