GoGrab Space Routing Architecture
Complete guide to the Space-First routing system with dynamic modules and event-driven navigation
πΊοΈ GoGrab Space Routing Architecture
Status: β
Production Ready
Last Updated: October 29, 2025
Architecture: Space-First with Event-Driven Navigation
π Table of Contents
- Overview
- Space-First Routing
- Dynamic Module Registration
- Event-Driven Navigation
- Storage & Caching
- Testing
- Related Documentation
π― Overview
The GoGrab Space routing system is built on three core principles:
- Space-First Architecture - All routes scoped under
:spaceSlug - Dynamic Module Registration - Routes injected at runtime
- Event-Driven Navigation - Smart rebuilding with flash-persistence
Why This Matters
Traditional routing:
/editor β No space context
/dashboard β No space context
/modularity β No space context
Our space-first routing:
/:spaceSlug/editor β
Space-scoped
/:spaceSlug/dashboard β
Space-scoped
/:spaceSlug/modularity β
Space-scoped
Benefits:
- β Multi-tenant support (each space is isolated)
- β Clean context separation
- β Better security (space-level guards)
- β Flexible deployment (same code, different spaces)
π Space-First Routing
Route Structure
// apps/gograb-space/src/app/app.routes.ts
export const gSpaceMainRoutes: Routes = [
{
path: ':spaceSlug', // π― Space-first parent route
loadComponent: () => AppShellComponent,
canActivate: [authGuard, spaceGuard],
resolve: { space: spaceResolver },
children: [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', loadComponent: DashboardComponent },
// Module routes injected here dynamically
]
}
];
URL Examples
Production:
/my-space/dashboard β User's space dashboard
/my-space/editor β Space-scoped editor
/team-workspace/modularity β Team's modularity view
Development:
/editor/dashboard β Editor workspace
/launch-pad/missions β Launch Station missions
Space Context Service
The UniverseContextService provides reactive space context:
const currentSpace = this.universeContext.currentSpace(); // "my-space"
const spacePath = `/${currentSpace}/editor`; // "/my-space/editor"
π Dynamic Module Registration
The Challenge
GoGrab Space uses a modular architecture:
- Singularity (space management)
- Editor (content editing)
- Space Map (visual overview)
- Modularity (module configuration)
- Launch Station (deployment)
Problem: Module routes don't exist until runtime!
// At app initialization:
router.config = [
{ path: ':spaceSlug/dashboard' } // Only static routes
];
// After module registration:
router.config = [
{ path: ':spaceSlug/dashboard' },
{ path: ':spaceSlug/editor' }, // β
Injected
{ path: ':spaceSlug/space-map' }, // β
Injected
{ path: ':spaceSlug/modularity' } // β
Injected
];
Solution: Runtime Route Injection
File: libs/core/module-integrator/src/lib/navigation/route-registration.service.ts
@Injectable({ providedIn: 'root' })
export class RouteRegistrationService {
registerModuleRoutes(): RouteRegistrationResult {
const modules = this.registry.getAll();
const moduleRoutes: Routes = [];
for (const module of modules) {
if (module.routes && module.routes.length > 0) {
const basePath = module.manifest.id; // 'editor', 'space-map', etc.
moduleRoutes.push({
path: basePath,
children: module.routes
});
}
}
// Inject routes into :spaceSlug children
this.injectModuleRoutes(currentConfig, moduleRoutes);
this.router.resetConfig(updatedConfig);
}
}
Bootstrap Flow
1. App Starts
β
2. Static routes loaded
β
3. BrowserBootstrapperService.initialize()
β
4. RouteRegistrationService.registerModuleRoutes()
β
5. Module routes injected into :spaceSlug
β
6. Router config updated
β
7. RouteReadyService.notifyRoutesRegistered() β
β‘ Event-Driven Navigation
The Problem (Original Approach)
Attempt #1: setTimeout (β WRONG)
ngOnInit(): void {
setTimeout(() => {
this.navigationBuilder.refreshNavigation();
}, 100); // β Timing-dependent, fragile
}
Why It Failed:
- β Arbitrary delay
- β No guarantee routes are ready
- β Race conditions
- β Not intelligent
The Solution: Event-Driven Architecture
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β EVENT-DRIVEN NAVIGATION ARCHITECTURE β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β 1. RouteReadyService (Event Bus) β
β - Signal: routesReady() β
β - Observable: routesRegistered$ β
β - Method: notifyRoutesRegistered() β
β β
β 2. Bootstrap Process (Emitter) β
β - Registers module routes β
β - Emits event when complete β
β β
β 3. NavigationBuilderService (Listener) β
β - Watches routesReady signal via effect() β
β - Rebuilds navigation automatically β
β - Caches to flash-persistence β
β β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Implementation
1. RouteReadyService (Event Bus)
File: apps/gograb-space/src/app/services/route-ready.service.ts
@Injectable({ providedIn: 'root' })
export class RouteReadyService {
private readonly _routesReady = signal<boolean>(false);
readonly routesReady = this._routesReady.asReadonly();
private readonly _routesRegistered = new Subject<void>();
readonly routesRegistered$ = this._routesRegistered.asObservable();
notifyRoutesRegistered(): void {
this._routesReady.set(true);
this._routesRegistered.next();
}
}
2. Emit Event After Registration
File: apps/gograb-space/src/main.ts
provideAppInitializer(() => {
const bootstrapper = inject(BrowserBootstrapperService);
const routeReady = inject(RouteReadyService);
bootstrapper.initialize({...}).subscribe({
next: (result) => {
// π― Event emitted EXACTLY when routes are ready
routeReady.notifyRoutesRegistered();
}
});
})
3. React to Event
File: apps/gograb-space/src/app/services/navigation-builder.service.ts
constructor() {
this.loadFromCache(); // Instant load from cache
this.buildNavigationFromRoutes(); // Initial build
// π― Automatic rebuild when routes ready
effect(() => {
const ready = this.routeReady.routesReady();
if (ready) {
this.buildNavigationFromRoutes();
this.saveToCache(); // Update cache
}
});
}
Complete Flow
App Start
β
NavigationBuilderService.constructor()
ββ loadFromCache() β β‘ INSTANT from storage
ββ buildNavigationFromRoutes() β Static routes
ββ effect(() => ...) β Set up listener
β
BrowserBootstrapperService.initialize()
ββ Register module routes
ββ routeReady.notifyRoutesRegistered() β π EVENT!
β
NavigationBuilderService effect triggers
ββ buildNavigationFromRoutes() β β
Includes module routes
ββ saveToCache() β πΎ Cache for next time
β
β
Navigation complete with all routes!
πΎ Storage & Caching
Flash-Persistence Integration
Library: @gg-storage (flash-persistence system)
Configuration:
private readonly CACHE_KEY = 'navigation-items-cache-v1';
private readonly CACHE_TTL_MS = 1000 * 60 * 60 * 24; // 24 hours
Cache Strategy
1. Load from Cache (Instant)
private async loadFromCache(): Promise<void> {
const cached = await this.storage.get<{
items: NavigationItem[];
timestamp: number;
}>(this.CACHE_KEY);
if (cached && (Date.now() - cached.timestamp < this.CACHE_TTL_MS)) {
this.routeBasedItems.set(cached.items);
console.log(`β‘ Loaded ${cached.items.length} items from cache`);
} else {
await this.storage.remove(this.CACHE_KEY); // Expired
}
}
2. Save to Cache (After Rebuild)
private async saveToCache(): Promise<void> {
await this.storage.set(this.CACHE_KEY, {
items: this.routeBasedItems(),
timestamp: Date.now()
});
console.log(`πΎ Cached ${this.routeBasedItems().length} items`);
}
Performance Benefits
| Scenario | Before | After | Improvement |
|---|---|---|---|
| First Load | ~200ms build | ~200ms build | Baseline |
| Second Load | ~200ms rebuild | ~5ms from cache | 40x faster β‘ |
| Route Change | ~150ms rebuild | ~150ms rebuild | Same |
π§ͺ Testing
Unit Tests
Files:
apps/gograb-space/src/app/services/route-ready.service.spec.tsapps/gograb-space/src/app/services/navigation-builder.service.spec.ts
Coverage:
- β RouteReadyService signal and observable APIs
- β NavigationBuilderService route extraction
- β Storage integration (load, save, expiration)
- β Event-driven rebuilding
- β Error handling
Run Tests:
nx test gograb-space
Integration Testing
Manual Test Flow:
- Clear cache:
localStorage.clear() - Refresh browser
- Check console: "πΎ Cached X navigation items"
- Refresh again
- Check console: "β‘ Loaded X items from cache (Ys old)"
π Related Documentation
Core Systems
- Event-Driven Navigation - RouteReadyService event bus for reactive navigation updates
- Navigation Caching System - Flash-persistence integration for instant sidenav rendering
Implementation Fixes
- Dynamic Module Routes Event-Driven Fix - Event-driven solution for dynamic route registration
Progress Notes
- Session October 29, 2025 - Storage-system integration and unit testing completion
π― Key Takeaways
- Space-First - All routes scoped under
:spaceSlugfor multi-tenancy - Dynamic Modules - Routes injected at runtime by module system
- Event-Driven - No timing hacks, pure event-based coordination
- Flash-Persistence - 40x faster subsequent loads with intelligent caching
- Fully Tested - Unit tests for all critical paths
Status: β
Production Ready
Version: 2.0.0
Architecture: Space-First + Event-Driven + Flash-Persistence