🔧 Routing Architecture Fix - Development vs Production
Fix space slug routing for development mode before subdomain deployment
🔧 Routing Architecture Fix
Date: October 31, 2025
Status: 🚨 CRITICAL ISSUE
Priority: HIGH - Blocks proper development workflow
🚨 The Problem
Current Behavior (WRONG for Development)
URL: http://localhost:4200/demo/dashboard
Space determined from: URL path segment ("demo")
Issue: Space is NOT tied to user - anyone can access any space!
Why This Is Wrong:
- Space should be determined by USER'S selection, not URL
- User authentication doesn't matter - URL controls space
- No space selector for users with multiple spaces
- Users can "hijack" spaces by changing URL
📊 The Two Deployment Models
1. Development/Shared Domain (CURRENT NEED ⚠️)
Environment:
- Domain:
localhost:4200ordev.gograb.space - All users share same domain
- Space determined by USER CONTEXT
Routing Pattern:
/dashboard ← No space slug in URL!
/editor
/space-map
/modularity
Flow:
1. User logs in
2. System checks: How many spaces does user have?
- ONE space → Auto-select, navigate to /dashboard
- MULTIPLE spaces → Show space selector modal
- ZERO spaces → Show "Create Space" wizard
3. User navigates app with selected space in context
4. Space stored in: UserContext service (signals)
5. URLs: /dashboard, /editor, etc. (NO space slug)
2. Production/Subdomain (FUTURE 🚀)
Environment:
- Domain:
{space}.gograb.space - Each space has own subdomain
- Space determined by SUBDOMAIN
Routing Pattern:
myspace.gograb.space/dashboard
myspace.gograb.space/editor
myspace.gograb.space/space-map
Flow:
1. User visits myspace.gograb.space
2. System parses subdomain → Space = "myspace"
3. User logs in (or uses existing session)
4. System validates user has access to "myspace"
5. User navigates app
6. URLs: /dashboard, /editor, etc. (space from subdomain)
🔧 Required Changes
1. Add Deployment Mode Configuration
File: apps/gograb-space/src/environments/environment.ts
export const environment = {
production: false,
deploymentMode: 'development', // 'development' | 'subdomain'
// Development mode settings
development: {
requireSpaceSlugInUrl: false, // ← KEY FLAG
useUserContextForSpace: true,
showSpaceSelector: true
},
// Subdomain mode settings
subdomain: {
requireSpaceSlugInUrl: false, // Subdomain determines space
useSubdomainForSpace: true,
baseDomain: 'gograb.space'
}
};
2. Update Route Configuration
File: apps/gograb-space/src/app/app.routes.ts
BEFORE (Current - Wrong for dev):
export const gSpaceMainRoutes: Routes = [
// ... public routes ...
{
path: ':spaceSlug', // 🚨 Takes slug from URL
loadComponent: () => import('./core/layout/main-shell/main-shell.component'),
canActivate: [authGuard, spaceGuard],
children: [
{ path: 'dashboard', component: GSpaceDashboardComponent },
{ path: 'editor', children: EditorRoutes },
// ... other tools ...
]
}
];
AFTER (Dual mode support):
import { environment } from '../environments/environment';
// Development mode: Routes at root level (no space slug)
const developmentRoutes: Routes = [
{
path: '', // Root level - no :spaceSlug
loadComponent: () => import('./core/layout/main-shell/main-shell.component'),
canActivate: [authGuard, spaceGuard], // spaceGuard gets space from context
children: [
{ path: 'dashboard', component: GSpaceDashboardComponent },
{ path: 'editor', children: EditorRoutes },
{ path: 'space-map', children: spaceMapRoutes },
{ path: 'singularity', children: SingularityRoutes },
{ path: 'modularity', children: ModularityRoutes },
]
}
];
// Subdomain mode: Routes with :spaceSlug (future)
const subdomainRoutes: Routes = [
{
path: ':spaceSlug', // For backward compatibility
loadComponent: () => import('./core/layout/main-shell/main-shell.component'),
canActivate: [authGuard, spaceGuard],
children: [
{ path: 'dashboard', component: GSpaceDashboardComponent },
{ path: 'editor', children: EditorRoutes },
{ path: 'space-map', children: spaceMapRoutes },
{ path: 'singularity', children: SingularityRoutes },
{ path: 'modularity', children: ModularityRoutes },
]
}
];
export const gSpaceMainRoutes: Routes = [
// Public routes (same for both modes)
{ path: '', redirectTo: '/landing', pathMatch: 'full' },
{ path: 'landing', component: LandingComponent },
{ path: 'auth', children: authRoutes },
// Protected routes - mode-dependent
...(environment.deploymentMode === 'development'
? developmentRoutes
: subdomainRoutes)
];
3. Update spaceGuard
File: apps/gograb-space/src/app/core/guards/space.guard.ts
Add mode detection:
import { environment } from '../../../environments/environment';
export const spaceGuard: CanActivateFn = async (route, state) => {
const spaceService = inject(SpaceService);
const userContext = inject(UserContextService);
const router = inject(Router);
// Development mode: Get space from user context
if (environment.deploymentMode === 'development') {
// Check if user has selected a space
const currentSpace = userContext.selectedSpace();
if (!currentSpace) {
// No space selected - show space selector
await router.navigate(['/select-space']);
return false;
}
// Validate user has access to this space
const hasAccess = await spaceService.validateUserSpaceAccess(
userContext.userId(),
currentSpace.id
);
if (!hasAccess) {
console.error('[spaceGuard] User does not have access to space');
await router.navigate(['/landing']);
return false;
}
// Set space context
spaceService.setCurrentSpace(currentSpace);
return true;
}
// Subdomain mode: Get space from subdomain or URL
else {
// Extract space from subdomain or :spaceSlug param
let spaceIdentifier: string | null = null;
// Try subdomain first
const subdomain = window.location.hostname.split('.')[0];
if (subdomain && subdomain !== 'localhost' && subdomain !== 'www') {
spaceIdentifier = subdomain;
}
// Fallback to :spaceSlug param
if (!spaceIdentifier) {
spaceIdentifier = route.paramMap.get('spaceSlug');
}
if (!spaceIdentifier) {
await router.navigate(['/landing']);
return false;
}
// Load space and validate access
const space = await spaceService.getSpaceBySlug(spaceIdentifier);
if (!space) {
console.error('[spaceGuard] Space not found:', spaceIdentifier);
await router.navigate(['/landing']);
return false;
}
// Validate user has access
const userId = userContext.userId();
const hasAccess = await spaceService.validateUserSpaceAccess(userId, space.id);
if (!hasAccess) {
console.error('[spaceGuard] User does not have access to space');
await router.navigate(['/landing']);
return false;
}
// Set space context
spaceService.setCurrentSpace(space);
return true;
}
};
4. Create Space Selector Component
File: apps/gograb-space/src/app/components/space-selector/space-selector.component.ts
import { Component, inject, signal } from '@angular/core';
import { Router } from '@angular/router';
import { SpaceService } from '@libs-foundational/core-angular';
import { UserContextService } from '@libs-foundational/core-angular';
@Component({
selector: 'app-space-selector',
standalone: true,
template: `
<div class="space-selector-overlay">
<div class="space-selector-modal">
<h2>Select Your Space</h2>
@if (loading()) {
<div class="loading">Loading your spaces...</div>
} @else if (spaces().length === 0) {
<div class="no-spaces">
<p>You don't have any spaces yet.</p>
<button (click)="createSpace()">Create Your First Space</button>
</div>
} @else {
<div class="spaces-grid">
@for (space of spaces(); track space.id) {
<div class="space-card" (click)="selectSpace(space)">
<div class="space-icon">{{ space.icon }}</div>
<h3>{{ space.name }}</h3>
<p>{{ space.description }}</p>
</div>
}
</div>
}
</div>
</div>
`
})
export class SpaceSelectorComponent {
private spaceService = inject(SpaceService);
private userContext = inject(UserContextService);
private router = inject(Router);
loading = signal(true);
spaces = signal<any[]>([]);
async ngOnInit() {
// Load user's spaces
const userId = this.userContext.userId();
const userSpaces = await this.spaceService.getUserSpaces(userId);
this.spaces.set(userSpaces);
this.loading.set(false);
// Auto-select if only one space
if (userSpaces.length === 1) {
await this.selectSpace(userSpaces[0]);
}
}
async selectSpace(space: any) {
// Set selected space in context
this.userContext.setSelectedSpace(space);
this.spaceService.setCurrentSpace(space);
// Navigate to dashboard
await this.router.navigate(['/dashboard']);
}
async createSpace() {
await this.router.navigate(['/universe/create']);
}
}
5. Update Navigation Links
All navigation links must be mode-aware:
// Development mode
const link = '/dashboard'; // No space slug
// Subdomain mode
const spaceSlug = getCurrentSpaceSlug();
const link = `/${spaceSlug}/dashboard`; // With space slug
Helper function:
export function getToolRoute(toolPath: string): string {
if (environment.deploymentMode === 'development') {
return `/${toolPath}`; // /dashboard, /editor, etc.
} else {
const spaceSlug = getCurrentSpaceSlug();
return `/${spaceSlug}/${toolPath}`; // /demo/dashboard, etc.
}
}
📋 Implementation Checklist
Phase 1: Configuration (1 hour)
- Add deployment mode to environment files
- Create development and production configs
- Add feature flags for each mode
Phase 2: Route Updates (2 hours)
- Split routes into developmentRoutes and subdomainRoutes
- Update app.routes.ts to use mode-dependent routes
- Update all route exports
Phase 3: Guard Updates (2 hours)
- Update spaceGuard for dual mode support
- Implement user-context space resolution (development)
- Implement subdomain space resolution (production)
Phase 4: Space Selector (3 hours)
- Create SpaceSelectorComponent
- Add route for /select-space
- Implement auto-select for single space
- Implement multi-space selector UI
Phase 5: Navigation Updates (2 hours)
- Create getToolRoute() helper
- Update all navigation links
- Update NavBuilderService
- Update space header component
Phase 6: Testing (2 hours)
- Test development mode with single space
- Test development mode with multiple spaces
- Test development mode with no spaces
- Verify all tools accessible
- Test navigation between tools
🎯 Expected Behavior After Fix
Development Mode (localhost:4200)
Login Flow:
1. User logs in at /auth/login
2. System checks user's spaces
3. ONE space → Auto-select, redirect to /dashboard
4. MULTIPLE spaces → Show space selector at /select-space
5. User selects space
6. Redirect to /dashboard
Navigation:
Dashboard: /dashboard
Editor: /editor
Space Map: /space-map
Modularity: /modularity
Space Context:
- Stored in: UserContextService (signals)
- Persisted in: localStorage or FlashPersistence
- Never in URL
Subdomain Mode (production.gograb.space)
URL Pattern:
myspace.gograb.space/dashboard
workspace.gograb.space/editor
demo.gograb.space/modularity
Login Flow:
1. User visits myspace.gograb.space
2. System extracts space from subdomain
3. User logs in
4. System validates user has access to "myspace"
5. Redirect to /dashboard (subdomain already set)
🚀 Migration Path
Step 1: Immediate (Development Mode)
- Set
deploymentMode: 'development'in environment - Update routes to remove :spaceSlug requirement
- Implement space selector
- Test with current users
Step 2: Later (Subdomain Mode)
- Set up DNS for subdomains
- Configure subdomain routing
- Set
deploymentMode: 'subdomain' - Test subdomain space resolution
- Deploy to production
💡 Key Insights
Why This Matters
- Security: Space access tied to user authentication
- UX: Natural flow - select space first, then navigate
- Scalability: Supports both shared and isolated deployments
- Flexibility: Easy to switch between modes
Architecture Benefits
- Single Codebase: One app supports both modes
- Feature Flag: Easy to toggle between modes
- Clean Separation: Development vs production concerns separated
- Future-Proof: Ready for subdomain deployment
📊 Comparison
| Aspect | Current (Broken) | Development Mode | Subdomain Mode |
|---|---|---|---|
| URL Pattern | /demo/dashboard |
/dashboard |
demo.gograb.space/dashboard |
| Space From | URL segment ❌ | User context ✅ | Subdomain ✅ |
| Security | URL-based ❌ | User-based ✅ | Subdomain + User ✅ |
| Multiple Spaces | No support ❌ | Space selector ✅ | Different subdomains ✅ |
| User Experience | Confusing ❌ | Natural ✅ | Professional ✅ |
🎯 Success Criteria
Development Mode Working When:
- User logs in and sees space selector (if multiple spaces)
- Space auto-selected if only one space
- URLs are clean:
/dashboard,/editor, etc. - Space persists across page refreshes
- All tools accessible without space slug
- Cannot access other user's spaces by changing URL
Subdomain Mode Working When:
- Space determined from subdomain
- User validated against subdomain space
- URLs are clean:
/dashboard(subdomain handles space) - Multiple spaces = multiple subdomains
- Proper 404 for invalid subdomains
Last Updated: October 31, 2025, 8:00 PM
Status: 🚨 ACTION REQUIRED
Priority: HIGH
Estimated Time: 12-15 hours
"Fix the foundation before building the Lamborghini!" 🏎️