Module System
The Ever Works platform is organized as a NestJS monorepo with a layered module architecture. This guide covers how feature modules, shared modules, dynamic modules, and global modules are structured, and how to avoid common pitfalls like circular dependencies.
Module Hierarchy
ApiModule (Root)
|
+-- AuthModule Feature module (controllers + services)
| +-- PassportModule
| +-- JwtModule.registerAsync()
| +-- DatabaseModule
|
+-- WorksModule Feature module
| +-- DatabaseModule
| +-- FacadesModule
| +-- MailModule
|
+-- MonitoringModule.forRoot() Global dynamic module
| +-- SentryModule.forRoot()
| +-- PostHogModule.forRoot()
|
+-- ThrottlerModule.forRoot() Global (from @nestjs/throttler)
+-- ScheduleModule.forRoot() Global (from @nestjs/schedule)
+-- EventEmitterModule.forRoot() Global (from @nestjs/event-emitter)
+-- CacheFactory.TypeORM() Global cache
+-- AgentPluginsModule.forRoot() Global plugin system
|
+-- SubscriptionsModule
+-- NotificationsModule
+-- ScreenshotModule
+-- PluginsModule
+-- GitProviderModule
+-- OAuthModule
+-- DeployModule
+-- AiConversationModule
+-- TriggerInternalModule
+-- TwentyCrmModule.forRoot()
Module Categories
Feature Modules
Feature modules encapsulate a domain area with its controllers, services, and DTOs:
// apps/api/src/auth/auth.module.ts
@Module({
imports: [
PassportModule,
DatabaseModule,
HttpModule,
JwtModule.registerAsync({
useFactory: () => ({
secret: jwtConstants.secret(),
signOptions: { expiresIn: jwtConstants.accessTokenExpiration() }
})
})
],
providers: [
AuthService,
LocalStrategy,
JwtStrategy,
GithubAuthStrategy,
GoogleAuthStrategy,
TokenCleanupService,
OAuthUrlService
],
controllers: [OAuthController, AuthController],
exports: [AuthService]
})
export class AuthModule {}
Key characteristics:
- Declares its own controllers
- Imports only the modules it needs
- Exports only the services other modules need
Shared Modules
Shared modules provide reusable services with no controllers. They are imported by multiple feature modules:
// packages/agent/src/database/database.module.ts
@Module({
imports: [
ConfigModule.forFeature(databaseConfig),
TypeOrmModule.forRootAsync({
/* ... */
}),
TypeOrmModule.forFeature(ENTITIES)
],
providers: [
WorkRepository,
UserRepository,
RefreshTokenRepository
// ... all repositories
],
exports: [
TypeOrmModule,
WorkRepository,
UserRepository,
RefreshTokenRepository
// ... all repositories
]
})
export class DatabaseModule {}
DatabaseModule is imported by AuthModule, WorksModule, FacadesModule, and others. TypeORM handles deduplication so the database connection is created only once.
Global Modules
Modules decorated with @Global() are available everywhere without explicit imports:
// packages/monitoring/src/monitoring.module.ts
@Global()
@Module({})
export class MonitoringModule {
static forRoot(config?: MonitoringConfig) {
return {
module: MonitoringModule,
global: true,
imports: [SentryModule.forRoot(config?.sentry), PostHogModule.forRoot(config?.posthog)],
providers: [AnalyticsService, SentryService],
exports: [AnalyticsService, SentryService]
};
}
}
After MonitoringModule.forRoot() is imported in ApiModule, AnalyticsService and SentryService are injectable in any module without additional imports.
Dynamic Modules
Dynamic modules use static methods (forRoot, forRootAsync, forFeature) to accept configuration:
// Usage in ApiModule
ThrottlerModule.forRoot(throttlerConfig),
MonitoringModule.forRoot({
sentry: { dsn: process.env.SENTRY_DSN },
posthog: { apiKey: process.env.POSTHOG_API_KEY },
}),
CacheFactory.TypeORM({ isGlobal: true }),
TwentyCrmModule.forRoot(),
AgentPluginsModule.forRoot(),
The forRoot() / forRootAsync() pattern is used for one-time configuration at the application root. The forFeature() pattern is used for per-module configuration.
The Facades Module
The FacadesModule demonstrates the pattern of bundling related services:
const FACADES = [
AiFacadeService,
SearchFacadeService,
ScreenshotFacadeService,
ContentExtractorFacadeService,
DataSourceFacadeService,
GitFacadeService,
OAuthFacadeService,
DeployFacadeService
];
@Module({
imports: [DatabaseModule],
providers: FACADES,
exports: FACADES
})
export class FacadesModule {}
Note that the FacadesModule relies on PluginsModule being registered globally -- it does not import it directly. This avoids a circular dependency since facades are consumed by pipeline modules that are also part of the plugin system.
Application Bootstrap
The ApiModule uses OnApplicationBootstrap to run post-initialization logic:
@Module({
/* imports, providers */
})
export class ApiModule implements OnApplicationBootstrap {
constructor(private readonly pluginBootstrap: PluginBootstrapService) {}
async onApplicationBootstrap(): Promise<void> {
await this.pluginBootstrap.bootstrap();
}
}
This is the single entry point for plugin discovery and loading. It runs after all modules have been initialized, ensuring all dependencies are available.
Workspace Package Modules
The monorepo structure maps to NestJS modules via workspace packages:
packages/agent/ --> DatabaseModule, FacadesModule, PluginsModule, ...
packages/monitoring/ --> MonitoringModule
packages/tasks/ --> TriggerModule, worker modules
packages/plugin/ --> Plugin contracts (no NestJS module, just types)
packages/contracts/ --> Shared TypeScript types
Import workspace packages using the @ever-works/* alias:
import { DatabaseModule, UserRepository } from '@ever-works/agent/database';
import { MonitoringModule } from '@ever-works/monitoring';
import { config } from '@ever-works/agent/config';
Avoiding Circular Dependencies
Problem
ModuleA imports ModuleB
ModuleB imports ModuleA
--> Circular dependency error
Solutions
- Extract shared logic -- Move the shared service into a third module that both can import:
ModuleA --> SharedModule
ModuleB --> SharedModule
- Use forwardRef() (last resort):
@Module({
imports: [forwardRef(() => ModuleB)]
})
export class ModuleA {}
- Use events -- Instead of direct cross-module calls, use the EventEmitter:
// ModuleA emits an event
this.eventEmitter.emit('work.created', new WorkCreatedEvent(work));
// ModuleB listens for the event (no import of ModuleA needed)
@OnEvent('work.created')
handleWorkCreated(event: WorkCreatedEvent) { /* ... */ }
The platform uses EventEmitterModule extensively to decouple modules.
Module Registration Checklist
When creating a new feature module:
1. [ ] Create the module class with @Module()
2. [ ] List all providers (services, repositories)
3. [ ] List controllers
4. [ ] Import required dependency modules
5. [ ] Export only the services consumed by other modules
6. [ ] Register the module in ApiModule imports
7. [ ] If globally needed, use @Global() or forRoot() with global: true
Best Practices
-
One module per domain -- Group related controllers, services, and repositories into a single feature module.
-
Minimal exports -- Only export what other modules actually need. Internal helper services should remain private.
-
Use
forRootfor configuration -- Any module that needs runtime configuration should use the dynamic module pattern. -
Prefer
@Global()sparingly -- Only monitoring, caching, and the plugin registry are truly global. Feature modules should be explicit about their imports. -
Events over imports -- When two modules need to communicate but should not depend on each other, use the event emitter.
Troubleshooting
Module not found when importing
Ensure the workspace package is built. Run pnpm build from the root, or build the specific package with turbo build --filter=@ever-works/agent.
Provider not available in module
Check that the provider is listed in exports of the source module AND that the source module is listed in imports of the consuming module.
Duplicate database connections
If DatabaseModule is imported in multiple places, TypeORM deduplicates the connection. If you see duplicate connection warnings, ensure TypeOrmModule.forRoot is called only once (in DatabaseModule), and other modules use TypeOrmModule.forFeature.
Related Documentation
- Dependency Injection -- Provider types and scoping
- Middleware Pipeline -- How modules plug into the request pipeline
- Configuration Management -- ConfigModule integration
- Performance Tuning -- Build optimization with Turborepo