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 FirestoreFirebaseAnalyticsService- Wraps existing Analytics serviceFirebaseUserService- Wraps UserProfileAPI
Stub Services (src/services/stub/)¶
EmbeddedConfigProvider- Returns config fromwindow.__XRGALLERY_CONFIG__NoopAnalyticsService- All tracking methods are no-opsStubUserService- 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¶
Builds the multi-page application (index.html, viewer.html, dashboard/, etc.)
Viewer Bundle Build¶
Creates dist/viewer-bundle.iife.js - a self-contained IIFE bundle that can be embedded in exported HTML files.
Full Build¶
Runs both builds sequentially.
Export Process (src/export/TourExporter.ts)¶
When a user exports a tour:
- Fetch Bundle: Get
viewer-bundle.iife.jsfrom/dist/ - Extract Assets: Get CSS and HTML body from
viewer.html - Generate HTML: Create standalone HTML with:
- Extracted CSS in
<style>tag - Viewer HTML body
- Embedded config in
window.__XRGALLERY_CONFIG__ - Bundled viewer code in
<script>tag - Create ZIP: Package with
config.jsonbackup andREADME.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¶
- No Credentials: Exported packages don't include Firebase config
- Runtime Detection: Services are chosen based on what's available at runtime
- 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¶
-
Build the viewer bundle:
-
Run the dev server:
-
Use the export feature from the dashboard
Testing Hosted Mode¶
Just run:
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¶
- Code Splitting: Could reduce hosted viewer load time by lazy-loading features
- CDN for Exports: Option to load viewer from CDN instead of embedding
- Partial Analytics: Basic anonymous analytics for exports via different service