Project Persistence & Root URL Architecture
How project selection persists across sessions and URLs remain at root
đ¯ Project Persistence & Root URL Architecture
Date: October 28, 2025
Time: 10:57 AM PST
Status: â
COMPLETE & WORKING
Purpose: Document how project selection persists and how URLs remain clean at root
đ Overview
The docs-angular application now implements true project-scoped architecture where:
- â Project context stored in state (NGXS + localStorage)
- â
URLs remain clean at root (
/instead of/ProjectName/) - â Persistence works across browser sessions
- â No redirects when project is loaded
đ How It Works
1. User Flow
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â First Visit (No Persisted Project) â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¤
â 1. User goes to http://localhost:5010/ â
â 2. APP_INITIALIZER loads: No project found â
â 3. Routes: No hardcoded redirects â
â 4. Catch-all route matches with projectSelectedGuard â
â 5. Guard checks: No project â Redirect to /select-project â
â 6. User sees project selection page â
â 7. User selects "GoGrab-Space" â
â 8. Action dispatched: SelectProject('english/GoGrab-Space')â
â 9. localStorage stores: {"projectId": "...", timestamp} â
â 10. Router navigates to: / (just root!) â
â 11. Guard allows: Project selected â
â
â 12. DynamicPageComponent loads content for GoGrab-Space â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
â Subsequent Visits (Persisted Project Exists) â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ¤
â 1. User goes to http://localhost:5010/ â
â 2. APP_INITIALIZER loads: Project found! â
â
â 3. ProjectSelectionState restored from localStorage â
â 4. Routes: NO redirects (removed hardcoded ones) â
â 5. Catch-all route matches with projectSelectedGuard â
â 6. Guard checks: Project selected â
â
â 7. DynamicPageComponent loads content for GoGrab-Space â
â 8. User stays at http://localhost:5010/ â
â
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
đī¸ Key Components
1. ProjectSelectionState (NGXS State)
File: apps/docs-angular/src/app/state/project-selection.state.ts
Purpose: Centralized state management for project selection
Storage Key: docs-selected-project
Storage Format:
{
"projectId": "english/GoGrab-Space",
"timestamp": 1730148470000
}
Actions:
LoadPersistedProject- Load from localStorage on app startupSelectProject- Save to localStorage when user selectsClearProject- Remove from localStorageLoadAvailableProjects- Load from docs-folders-config.json
Key Features:
- â Automatic persistence to localStorage
- â Validates persisted project still exists
- â Cleans up invalid/stale selections
- â Includes project metadata (language, visibility, config)
@Action(SelectProject)
async selectProject(ctx: StateContext<ProjectSelectionStateModel>, action: SelectProject) {
const state = ctx.getState();
const project = state.availableProjects.find(p => p.id === action.projectId);
if (!project) {
console.error('â Project not found:', action.projectId);
return;
}
// đ¯ PERSISTENCE: Store to localStorage
try {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify({
projectId: action.projectId,
timestamp: Date.now()
}));
} catch (error) {
console.warn('â ī¸ Failed to persist project selection:', error);
}
ctx.patchState({
currentProjectId: action.projectId,
currentProjectMetadata: project,
lastSelectedTimestamp: Date.now()
});
console.log('â
Project selected:', action.projectId);
}
2. APP_INITIALIZER (App Startup)
File: apps/docs-angular/src/app/app.config.ts
Purpose: Load persisted project before app renders
Timing: Runs BEFORE any routing happens
Logic:
provideAppInitializer(() => {
const store = inject(Store);
const router = inject(Router);
return (async () => {
try {
console.log('đ Loading persisted project...');
await store.dispatch(new LoadPersistedProject()).toPromise();
const isProjectSelected = store.selectSnapshot(/* check state */);
const currentPath = router.url;
// đ¯ PROJECT-SCOPED ARCHITECTURE: Navigation rules
// 1. If project IS selected â Allow navigation to proceed
// 2. If project NOT selected and not on selection page â Redirect
// 3. If already on selection page â Do nothing
if (!isProjectSelected && !currentPath.includes('/select-project')) {
console.log('âšī¸ No project selected, redirecting to selection page');
setTimeout(() => {
router.navigate(['/select-project']);
}, 100);
} else if (isProjectSelected) {
console.log('â
Project loaded from persistence, allowing navigation');
}
return true;
} catch (error) {
console.error('â Failed to load persisted project:', error);
return true;
}
})();
})
Why Delayed Redirect?
setTimeout()allows router to fully initialize- Prevents race conditions with route resolution
- Ensures guard can properly evaluate state
3. Dynamic Routes Service
File: apps/docs-angular/src/app/services/dynamic-routes.service.ts
Purpose: Generate routes WITHOUT hardcoded redirects
Critical Change: Removed hardcoded root redirects
Before â:
{
path: '',
redirectTo: '/select-project',
pathMatch: 'full'
},
{
path: 'es',
redirectTo: '/select-project',
pathMatch: 'full'
}
After â :
// đ¯ PROJECT-SCOPED ARCHITECTURE: Root paths handled by catch-all + guard
// NO redirects here - let guard decide based on project state
// If project selected â show content at root
// If no project â guard redirects to /select-project
Why This Matters:
- Root path (
/) now matches catch-all route - Catch-all route protected by
projectSelectedGuard - Guard checks state and decides action
- Result: Root stays at
/when project is loaded! â
4. Project Selected Guard
File: apps/docs-angular/src/app/guards/project-selected.guard.ts
Purpose: Enforce project selection before showing content
Logic:
export const projectSelectedGuard: CanActivateFn = (route) => {
const store = inject(Store);
const router = inject(Router);
// â
ALWAYS ALLOW: /select-project bypasses checks
const currentPath = route.url.map(segment => segment.path).join('/');
if (currentPath === 'select-project' || currentPath.startsWith('select-project')) {
return true;
}
const isProjectSelected = store.selectSnapshot(ProjectSelectionState.isProjectSelected);
const isInitialized = store.selectSnapshot(ProjectSelectionState.isInitialized);
// Wait for initialization
if (!isInitialized) {
return router.createUrlTree(['/select-project']);
}
// Check if project is selected
if (!isProjectSelected) {
return router.createUrlTree(['/select-project']);
}
// â
Project selected - allow navigation
return true;
};
When It Runs:
- Every navigation attempt to protected routes
- Catch-all route (
**) uses this guard - Root path (
/) hits catch-all, triggers guard
Decision Flow:
User navigates to /
â
Catch-all route matches
â
projectSelectedGuard runs
â
Is project selected in state?
ââ YES â Allow navigation â
â DynamicPageComponent renders
â Shows content for selected project
â
ââ NO â Redirect to /select-project
User must select a project
5. Project Selection Component
File: apps/docs-angular/src/app/pages/project-selection/project-selection.component.ts
Purpose: UI for selecting projects
Critical Change: Navigation after selection
Before â:
const projectPath = `${langPrefix}/${project.name}`;
await this.router.navigate([projectPath]);
// Result: http://localhost:5010/GoGrab-Space â
After â :
// đ¯ PROJECT-SCOPED ARCHITECTURE: Navigate to ROOT with project context in state only
const langPrefix = project.language === 'spanish' ? '/es' : '';
const rootPath = langPrefix || '/'; // Just "/" or "/es"
console.log('đ Navigating to project root (state-based):', rootPath);
console.log('đĻ Project context stored in state:', project.id);
await this.router.navigate([rootPath]);
// Result: http://localhost:5010/ â
Why This Works:
- Project context stored in NGXS state
- URL remains clean at root
- Guard sees project is selected
- DynamicPageComponent reads project from state
- Content loaded for correct project
đ Debugging Tips
Check Persistence
Browser DevTools â Application â Local Storage
Look for key: docs-selected-project
Expected Value:
{
"projectId": "english/GoGrab-Space",
"timestamp": 1730148470000
}
Console Logs
Successful Load:
đ Loading persisted project...
â
Loaded persisted project: english/GoGrab-Space
đ APP_INITIALIZER status: {
isProjectSelected: true,
loadedProjectId: "english/GoGrab-Space",
currentPath: "/",
isSelectProjectPage: false
}
â
Project loaded from persistence, allowing navigation to: /
â
Guard: Project selected, allowing navigation
No Project (First Visit):
đ Loading persisted project...
âšī¸ No persisted project found
đ APP_INITIALIZER status: {
isProjectSelected: false,
loadedProjectId: null,
currentPath: "/",
isSelectProjectPage: false
}
âšī¸ No project selected, redirecting to selection page
đĢ Guard: No project selected, redirecting to selection page
đ¯ Benefits
1. Clean URLs
- â
User sees:
http://localhost:5010/ - â
Not:
http://localhost:5010/GoGrab-Space/ - â State manages context, not URLs
2. Persistence
- â Project selection survives browser refresh
- â User doesn't re-select every time
- â Fast loading - no unnecessary navigation
3. Separation of Concerns
- â State layer handles persistence
- â Routes remain simple and clean
- â Guard enforces business rules
- â Components render based on state
4. Performance
- â No unnecessary redirects
- â Instant load from localStorage
- â Validation ensures data integrity
đ§ Configuration
Storage Key
Change in project-selection.state.ts:
private readonly STORAGE_KEY = 'docs-selected-project';
NGXS Persistence
Configure in app.config.ts:
NgxsStoragePluginModule.forRoot({
keys: ['projectDocs', 'projectSelection']
})
Persistence Strategy
Currently: localStorage (simple, fast)
Future Options:
- IndexedDB for larger data
- SessionStorage for temporary selections
- Cloud sync for multi-device
đ¨ Common Issues
Issue: Still Redirects to /select-project
Cause: Hardcoded redirects in route config
Solution: Check dynamic-routes.service.ts - ensure NO redirects for root paths
Verify:
private getBaseRoutes(): Route[] {
return [
// â
CORRECT: No hardcoded redirects
{ path: 'select-project', loadComponent: ... },
// â WRONG: Hardcoded redirect
// { path: '', redirectTo: '/select-project', pathMatch: 'full' }
];
}
Issue: URL has project name
Cause: Navigation in ProjectSelectionComponent uses project name
Solution: Navigate to root only
Verify:
// â
CORRECT
const rootPath = langPrefix || '/';
await this.router.navigate([rootPath]);
// â WRONG
await this.router.navigate([`/${project.name}`]);
Issue: Persistence not working
Cause: localStorage blocked or NGXS not configured
Checks:
- Browser DevTools â Application â Local Storage
- Check NGXS config in
app.config.ts - Verify
SelectProjectaction saves to localStorage - Check browser console for errors
â Testing Checklist
First Visit (No Persistence)
- Navigate to
http://localhost:5010/ - Should redirect to
/select-project - Select a project (e.g., "GoGrab-Space")
- URL becomes
http://localhost:5010/(just root!) - Content loads for selected project
- Check localStorage has
docs-selected-projectkey
Subsequent Visits (With Persistence)
- Refresh browser (F5 or Cmd+R)
- URL stays at
http://localhost:5010/ - Content loads immediately (no redirect)
- Console shows "â Project loaded from persistence"
- Content is for the correct project
Project Switching
- Click project switcher in navbar
- Select different project
- URL stays at root
- Content updates to new project
- localStorage updates with new project ID
Multiple Languages
- Select Spanish project
- URL becomes
http://localhost:5010/es - Refresh browser
- URL stays at
/es - Content loads in Spanish
đ Related Documentation
- PROJECT_SCOPED_ARCHITECTURE_COMPLETE.md - Complete architecture overview
- Access Control & Cross-Project Navigation - Project access rules
- NGXS State Management - External NGXS documentation
Last Updated: October 28, 2025
Maintained By: AI Agent + Development Team
Status: â
Production Ready