🔧 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:

  1. Space should be determined by USER'S selection, not URL
  2. User authentication doesn't matter - URL controls space
  3. No space selector for users with multiple spaces
  4. Users can "hijack" spaces by changing URL

📊 The Two Deployment Models

1. Development/Shared Domain (CURRENT NEED ⚠️)

Environment:

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:

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)

Phase 2: Route Updates (2 hours)

Phase 3: Guard Updates (2 hours)

Phase 4: Space Selector (3 hours)

Phase 5: Navigation Updates (2 hours)

Phase 6: Testing (2 hours)


🎯 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:


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)

  1. Set deploymentMode: 'development' in environment
  2. Update routes to remove :spaceSlug requirement
  3. Implement space selector
  4. Test with current users

Step 2: Later (Subdomain Mode)

  1. Set up DNS for subdomains
  2. Configure subdomain routing
  3. Set deploymentMode: 'subdomain'
  4. Test subdomain space resolution
  5. Deploy to production

💡 Key Insights

Why This Matters

  1. Security: Space access tied to user authentication
  2. UX: Natural flow - select space first, then navigate
  3. Scalability: Supports both shared and isolated deployments
  4. Flexibility: Easy to switch between modes

Architecture Benefits

  1. Single Codebase: One app supports both modes
  2. Feature Flag: Easy to toggle between modes
  3. Clean Separation: Development vs production concerns separated
  4. 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:

Subdomain Mode Working When:


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!" 🏎️