Module Registration System
Complete guide to module registration (visual and code-first)
Module Registration System
Updated: December 3, 2025
Status: ✅ Production System - Visual + Code-First
Purpose: Complete guide to module registration using both approaches
🎯 Executive Summary
The Production System: All modules in GoGrab Space use the BrowserModuleContract pattern with @ModuleDecorator for registration.
Two Creation Methods, One Standard:
- Visual Builder (
modularity-lib) - Generates BrowserModuleContract code - Code-First - Write BrowserModuleContract code manually
Both produce the exact same result: production-ready modules implementing BrowserModuleContract.
Key Facts:
- ✅ Visual + Code: Both methods produce identical module structure
- ✅ Production-Ready: Used in
gograb-adminand generated frommodularity-lib - ✅ Auto-Discovery: Modules self-register via decorators
- ✅ Code Export: Visual builder generates production-ready TypeScript
- ✅ Hybrid Approach: Start visual, refine with code
Module Creation Tools:
modularity-lib: ✅ Visual Module Workbench at/modularity- Manual: ✅ Code-first approach (traditional)
📊 Production System: BrowserModuleContract
📚 Source of Truth: All patterns must match
gograb-modules/modules/new-user-management/
🔍 Verify: Check gograb-modules implementation before following examples
⚠️ Updated: November 23, 2025 - Fixed array format forpublicActionsandembeddableComponents
Overview
Location: @libs-core/module-base
Used By: gograb-admin (production), gograb-space (migrating)
Status: ✅ Production-Ready
Architecture
// 1. Define a class implementing BrowserModuleContract
@Injectable({ providedIn: 'root' })
@ModuleDecorator({
id: 'my-module',
version: '1.0.0',
name: 'My Module',
description: 'Module description'
})
export class MyModule implements BrowserModuleContract {
readonly manifest: ModuleManifest = {
id: 'my-module',
name: 'My Module',
version: '1.0.0',
description: 'Module description',
routes: [...],
navigation: {...},
publicActions: [ // ✅ Array format (not object)
NavigateToItemAction,
CreateItemAction,
UpdateItemAction,
],
embeddableComponents: [ // ✅ Array format (not object)
{
id: 'my-module:list-widget',
componentType: MyListWidgetComponent,
displayName: 'My List Widget',
description: 'Displays a list of items',
category: 'widget',
inputs: [
{ name: 'limit', type: 'number', required: false },
],
outputs: [
{ name: 'itemSelected', type: 'object' },
],
},
]
};
configure<T = Record<string, unknown>>(config: T): Observable<void> {
void config;
return of(void 0);
}
initialize(): Observable<void> { return of(void 0); }
start(): Observable<void> { return of(void 0); }
stop(): Observable<void> { return of(void 0); }
destroy(): Observable<void> { return of(void 0); }
}
// 2. Import in central registry (apps/gograb-admin/src/app/core/modules.ts)
import { MyModule } from '@my-module';
const coreModules = [MyModule];
void coreModules; // Prevents tree-shaking
// 3. Import registry in app.config.ts
import './core/modules'; // Triggers @ModuleDecorator registration
// 4. Auto-discovery handles the rest
// - ModuleDiscoveryService finds all @ModuleDecorator classes
// - ModuleRegistryService stores them
// - BrowserBootstrapperService manages lifecycle
// - RouteRegistrationService injects routes dynamically
Key Files
Core Infrastructure:
libs/core/module-base/src/lib/contracts/browser-module.contract.ts- Contract interfacelibs/core/module-base/src/lib/decorators/module.decorator.ts- Auto-registration decoratorlibs/core/module-base/src/lib/services/module-registry.service.ts- Module storagelibs/core/module-base/src/lib/services/module-discovery.service.ts- Auto-discoverylibs/core/module-integrator/src/lib/bootstrap/browser-bootstrapper.service.ts- Lifecycle orchestration
Registration Flow
graph TB
A[@ModuleDecorator applied to class] --> B[Decorator registers in static registry]
B --> C[ModuleDiscoveryService.discoverAndRegister]
C --> D[ModuleRegistryService.register]
D --> E[BrowserBootstrapperService initializes lifecycle]
E --> F[RouteRegistrationService injects routes]
F --> G[Module fully operational ✅]
Characteristics
| Aspect | Details |
|---|---|
| Auto-Discovery | ✅ Yes - via @ModuleDecorator |
| Lifecycle Management | ✅ Full (configure → initialize → start → stop → destroy) |
| Route Injection | ✅ Dynamic route registration |
| Enterprise Features | ✅ Multi-tenant, configuration API, observability |
| Complexity | Medium (requires class implementation + lifecycle hooks) |
| Boilerplate | Moderate (offset by comprehensive features) |
| Production Status | ✅ PRODUCTION-READY |
| Used In | gograb-admin ✅, gograb-space (migrating) 🚧 |
Working Example (gograb-admin)
File: apps/gograb-admin/src/app/core/modules.ts
/**
* Module registration file.
* Import all modules here to trigger registration
* with the module-integrator system via their @ModuleDecorator decorators.
*/
import { UserManagementModule } from '@libs-core/user-management';
const coreModules = [UserManagementModule];
// Prevent tree-shaking
void coreModules;
File: apps/gograb-admin/src/app/app.config.ts
import './core/modules'; // ⭐ Triggers module registration
export const appConfig: ApplicationConfig = {
providers: [
// ... other providers
provideBootstrapInitializer(), // Initializes modules
// ...
]
};
Result: Module auto-discovered, routes injected, fully operational ✅
🚫 Deprecated Experiment: @module-system (Manifest-Only)
Overview
Status: ⚠️ Incomplete Experiment - Do Not Use
Location: libs/module-system (deprecated)
Attempted In: gograb-space only
Why It Exists
An attempt was made in gograb-space to simplify module registration by using plain manifests instead of full class implementations. However:
- ❌ Route injection doesn't work
- ❌ No lifecycle management
- ❌ Manual registration required
- ❌ Not integrated with core infrastructure
- ❌ Never completed
What Was Attempted
// Simplified manifest-only approach (INCOMPLETE)
export const MyModuleManifest: ModuleManifest = {
id: 'my-module',
name: 'My Module',
version: '1.0.0',
routes: [...],
navigation: {...}
};
// Manual registration in main.ts (DOESN'T WORK FULLY)
const moduleRegistry = inject(ModuleRegistryService);
moduleRegistry.registerModule(MyModuleManifest);
Why It Failed
- No Lifecycle Hooks: Can't manage initialization, startup, shutdown
- Route Injection Broken: Routes not dynamically injected
- No Auto-Discovery: Requires manual registration everywhere
- Incomplete Implementation: Never finished integrating with core system
- Dual System Confusion: Created confusion about which system to use
Current Status
Verdict: ❌ Do Not Use - Use BrowserModuleContract instead
Migration Path: All gograb-space modules should migrate to BrowserModuleContract pattern (see Migration Guide below)
🎨 Registration Method 1: Visual Builder
Access the Module Workbench
- Navigate to
/modularityin GoGrab Space - Click "New Module"
- Follow the 6-step guided process
- Export as TypeScript when complete
What Gets Generated
The Module Workbench automatically generates:
- ✅
BrowserModuleContractimplementation with all lifecycle methods - ✅
@ModuleDecoratorwith full metadata - ✅
@PublicActiondecorated action classes - ✅ Embeddable components with proper inputs/outputs
- ✅ Complete file structure ready for Nx library
Export and Integrate
- Export: Click "Export as Angular Library" in Deploy step
- Save: Download the generated files
- Place: Copy to
modules/your-module/in your monorepo - Build: Run
nx build your-module - Register: Add to
apps/gograb-space/src/app/core/modules.ts
Generated Code Quality:
- Matches gograb-modules reference implementations exactly
- Observable-based lifecycle methods
- Proper event emission
- Structured logging
- Type-safe throughout
💻 Registration Method 2: Code-First
Step 1: Generate Library
npx nx g @nx/angular:lib my-module --directory=modules --standalone
Note: All GoGrab Space modules use the @module/ namespace prefix. See Naming Convention for details.
Step 2: Implement BrowserModuleContract
File: modules/my-module/src/lib/my-module.module.ts
import { Injectable, inject } from '@angular/core';
import { Observable, of } from 'rxjs';
import { BrowserModuleContract, ModuleManifest, ModuleDecorator } from '@libs-core/module-base';
import { LoggerService } from '@libs-core/log-system';
@ModuleDecorator({
id: 'my-module',
version: '1.0.0',
displayName: 'My Module'
})
@Injectable({ providedIn: 'root' })
export class MyModule implements BrowserModuleContract {
private readonly logger = inject(LoggerService);
readonly manifest: ModuleManifest = {
id: 'my-module',
version: '1.0.0',
displayName: 'My Module',
routes: [
{
path: 'my-module',
loadComponent: () => import('./components/my-module.component')
.then(m => m.MyModuleComponent)
}
],
navigation: {
menuItems: [{
id: 'my-module',
label: 'My Module',
route: '/my-module',
icon: 'dashboard'
}]
}
};
configure(config: any): Observable<void> {
this.logger.info('MyModule', 'Configuring...', config);
return of(void 0);
}
initialize(): Observable<void> {
this.logger.info('MyModule', 'Initializing...');
return of(void 0);
}
start(): Observable<void> {
this.logger.info('MyModule', 'Starting...');
return of(void 0);
}
stop(): Observable<void> {
this.logger.info('MyModule', 'Stopping...');
return of(void 0);
}
destroy(): Observable<void> {
this.logger.info('MyModule', 'Destroying...');
return of(void 0);
}
}
Step 3: Register Module
File: apps/gograb-admin/src/app/core/modules.ts (or equivalent)
import { MyModule } from '@my-module';
const coreModules = [
UserManagementModule,
MyModule // ⭐ Add your module here
];
void coreModules;
Step 4: Import in App Config
File: apps/gograb-admin/src/app/app.config.ts
import './core/modules'; // ✅ Already done
Step 5: Run and Verify
# Start the app
npx nx serve gograb-admin
# Check console logs:
# [MyModule] Configuring...
# [MyModule] Initializing...
# [MyModule] Starting...
Result: Module auto-discovered, routes available, navigation shows up! ✅
🔄 Migration Guide (gograb-space)
Current State
gograb-space attempted to use manifest-only registration but it's incomplete.
Migration Steps
1. Create BrowserModuleContract Classes
For each module in gograb-space, convert from:
// ❌ OLD (incomplete)
export const SingularityManifest: ModuleManifest = { ... };
To:
// ✅ NEW (production)
@ModuleDecorator({ id: 'singularity', version: '1.0.0' })
@Injectable({ providedIn: 'root' })
export class SingularityModule implements BrowserModuleContract {
readonly manifest: ModuleManifest = { ... };
configure() { ... }
initialize() { ... }
start() { ... }
stop() { ... }
destroy() { ... }
}
2. Create Module Registry File
File: apps/gograb-space/src/app/core/modules.ts
import { SingularityModule } from '@libs-structural/singularity-lib';
import { SpaceMapModule } from '@space-map-lib';
import { EditorModule } from '@editor-web-lib';
// ... other modules
const platformModules = [
SingularityModule,
SpaceMapModule,
EditorModule
// ... other modules
];
void platformModules; // Prevents tree-shaking
3. Import in App Config
File: apps/gograb-space/src/app/app.config.ts
import './core/modules'; // ⭐ Add this line
export const appConfig: ApplicationConfig = {
providers: [
// ... existing providers
]
};
4. Remove Manual Registration
File: apps/gograb-space/src/main.ts
// ❌ REMOVE these manual registrations
// provideAppInitializer(() => {
// const moduleRegistry = inject(ModuleRegistryService);
// moduleRegistry.registerModule(SingularityManifest);
// moduleRegistry.registerModule(SpaceMapManifest);
// // ...
// });
5. Test Thoroughly
# Run gograb-space
npx nx serve gograb-space
# Verify in console:
# - Module discovery logs
# - Route registration
# - Navigation items appear
📚 Reference Documentation
Source of Truth
- GoGrab-Modules Module Creation Guide - Foundation team's guide
- GoGrab-Admin Verification - Working example
Related Documentation
- Module System Overview - System architecture
- Defining Modularity - Module philosophy
- Module Naming Convention -
@module/namespace standard - Module Architecture - Why modules are NX libraries
- BrowserModuleContract API - API reference
🚨 Important Lessons Learned
The Confusion (October 2025)
We discovered documentation labeled the systems backwards:
- ❌ Labeled
BrowserModuleContractas "OLD System" - ❌ Labeled
@module-systemas "NEW System" - ❌ Recommended migrating away from production system
Reality:
- ✅
BrowserModuleContractis the production system - ✅
@module-systemwas an incomplete experiment - ✅ We should be migrating to BrowserModuleContract, not away from it
How to Prevent This
- Trust the Source: gograb-modules team is the foundation - follow their patterns
- Verify with Code: Check working implementations (gograb-admin)
- Document Immediately: Real-time documentation prevents confusion
- Complete Migrations: Don't leave systems half-migrated
- Archive Deprecated Docs: Clear labeling of what's outdated
✅ Best Practices
Module Development
- Always implement BrowserModuleContract - No shortcuts
- Use @ModuleDecorator - Enables auto-discovery
- Implement all lifecycle hooks - Even if some are empty
- Follow gograb-modules patterns - They're the source of truth
- Test module lifecycle - Verify configure → start → stop → destroy
Registration
- Use central registry file (
core/modules.ts) - Import in app.config.ts - Triggers auto-discovery
- Let the system handle it - Don't manually register
- Check console logs - Verify discovery and initialization
- Test routes dynamically - Ensure route injection works
Documentation
- Document as you build - Real-time is best
- Link to source of truth - Reference gograb-modules docs
- Show working examples - Link to gograb-admin
- Update when patterns change - Keep docs current
- Archive deprecated - Don't delete, explain why it changed
📞 Questions?
If you're confused about module registration:
✅ Use: BrowserModuleContract with @ModuleDecorator
✅ Example: gograb-admin application
✅ Reference: GoGrab-modules documentation
❌ Don't Use: @module-system manifest-only registration
For Help: See Module Creation Guide
🎨 The Visual Builder Advantage
Why Use the Module Workbench?
- Guided Process: Never forget a step or misconfigure
- AI Assistance: Describe your module, AI generates structure
- Instant Validation: Real-time checks for completeness
- Code Generation: Production-ready TypeScript output
- Best Practices: Generated code follows all standards
- Flash Persistence: Your work auto-saves with real-time sync
- Entity-Aware: Actions and widgets understand your data model
- Visual Flow Builder: Build action logic without writing RxJS operators
When to Add Manual Code
After exporting from the visual builder, you might manually add:
- Complex business logic in action
execute()methods - Custom validation beyond generated patterns
- Advanced RxJS operators for specific workflows
- Integration with external APIs
- Performance optimizations
The 80/20 Rule: Visual builder handles 80% of the structure, you refine the 20% of complex logic.
Last Updated: December 3, 2025 - 9:50 PM EST Status: ✅ Current - Hybrid Approach (Visual + Code) Version: 3.0 (Visual Builder Integration) Session ID: 3