Skip to content

Module Registration

Feature modules are registered by passing them to createCruzApp() in your server entry file (server.cloudflare.ts for Cloudflare deployments):

server.cloudflare.ts
import { createCruzApp } from '@cruzjs/core';
import { CloudflareAdapter } from '@cruzjs/adapter-cloudflare';
import * as schema from './database/schema';
import { ProductModule } from './features/product/product.module';
import { AnalyticsModule } from './features/analytics/analytics.module';
import { NotificationModule } from './features/notifications/notification.module';
export default createCruzApp({
schema,
modules: [ProductModule, AnalyticsModule, NotificationModule],
adapter: new CloudflareAdapter(),
pages: () => import('virtual:react-router/server-build'),
});

Each module is a @Module() class that declares everything the feature contributes — services, tRPC routers, page routes, and event listeners:

import { Module } from '@cruzjs/core/di';
@Module({
providers: [ProductService, InventoryService],
trpcRouters: {
product: productRouter,
inventory: inventoryRouter,
},
events: [
{ event: ProductCreatedEvent, listener: updateSearchIndex },
{ event: ProductCreatedEvent, listener: notifyTeam },
],
})
export class ProductModule {}

No separate .provider.ts file is needed. The module is the only file required.

OptionTypePurpose
providersProvider[]Services, factories, and values to register in the DI container
trpcRoutersRecord<string, Router>tRPC routers to merge into the app router
pageRoutes(helpers) => RouteConfig[]React Router page routes
eventsEventBinding[]Event-to-listener mappings

Module order in the modules array determines load order. Register modules that other modules depend on first:

export default createCruzApp({
schema,
modules: [
// UserProfileModule must come first -- other features inject UserProfileService
UserProfileModule,
// ProductModule depends on UserProfileService for creator info
ProductModule,
// AnalyticsModule depends on ProductService
AnalyticsModule,
],
adapter: new CloudflareAdapter(),
pages: () => import('virtual:react-router/server-build'),
});

Core modules (SharedModule, AuthModule, JobModule, etc.) and framework modules (OrgModule, BillingModule, etc.) are loaded automatically before your app modules.

The @Module() decorator is the recommended way to declare providers, tRPC routers, page routes, and events:

import { Module } from '@cruzjs/core/di';
@Module({
providers: [ProductService, InventoryService],
trpcRouters: {
product: productRouter,
inventory: inventoryRouter,
},
pageRoutes: (helpers) => [
...helpers.prefix('products', [
helpers.index('features/product/routes/index.tsx'),
helpers.route(':id', 'features/product/routes/$id.tsx'),
]),
],
events: [
{ event: ProductCreatedEvent, listener: updateSearchIndex },
{ event: ProductCreatedEvent, listener: notifyTeam },
],
})
export class ProductModule {}

For many features, this is all you need — a single module file with no separate provider.

The pageRoutes factory receives route helpers (prefix, index, route, layout) and returns route configuration. Route files should live inside the feature directory:

src/features/product/
├── product.service.ts
├── product.router.ts
├── product.module.ts
├── routes/
│ ├── index.tsx # /products (list page)
│ └── $id.tsx # /products/:id (detail page)
└── index.ts

Routes declared in pageRoutes are automatically merged into the application’s route tree when the module is passed to createCruzApp() or createCruzRoutes().

In most cases, you will register routes directly in src/routes.ts instead of using pageRoutes:

src/routes.ts
...prefix('products', [
index('features/product/routes/index.tsx'),
route(':id', 'features/product/routes/$id.tsx'),
]),

The pageRoutes option is primarily useful for framework packages that need to contribute routes programmatically.

Some core tokens expect your application to provide an implementation. The most common is USER_HYDRATOR:

import { Injectable, Inject } from '@cruzjs/core/di';
import { IUserHydrator, USER_HYDRATOR } from '@cruzjs/core';
import { DRIZZLE, type DrizzleDatabase } from '@cruzjs/core/shared/database/drizzle.service';
import { userProfiles } from './user-profile.schema';
@Injectable()
export class UserProfileHydrator implements IUserHydrator {
constructor(@Inject(DRIZZLE) private readonly db: DrizzleDatabase) {}
async hydrate(identityId: string, email: string) {
const [profile] = await this.db
.select()
.from(userProfiles)
.where(eq(userProfiles.id, identityId))
.limit(1);
return { profile: profile ?? null };
}
}
@Module({
providers: [
UserProfileService,
UserProfileHydrator,
{ provide: USER_HYDRATOR, useClass: UserProfileHydrator },
],
trpcRouters: { userProfile: userProfileRouter },
})
export class UserProfileModule {}

Register job handlers using multi-injection so the job runner discovers them automatically:

import { Injectable } from '@cruzjs/core/di';
import { JOB_HANDLER, type JobHandler, type Job, type JobResult } from '@cruzjs/core/jobs';
@Injectable()
export class GenerateReportJobHandler implements JobHandler {
readonly metadata = {
jobType: 'generate-report',
statuses: ['PENDING'],
};
async run(job: Job): Promise<JobResult> {
const { orgId, reportType } = job.payload;
// ... generate the report
return { success: true };
}
}
@Module({
providers: [
ReportService,
{ provide: JOB_HANDLER, useClass: GenerateReportJobHandler, multi: true },
],
})
export class ReportModule {}

React to framework events without modifying core code:

import { IdentityCreatedEvent } from '@cruzjs/core';
import { OrganizationCreatedEvent, MemberAddedEvent } from '@cruzjs/pro';
@Module({
providers: [OnboardingService],
events: [
// Create a profile when a user registers
{ event: IdentityCreatedEvent, listener: createUserProfile },
// Set up defaults when an org is created
{ event: OrganizationCreatedEvent, listener: createDefaultResources },
// Send welcome message when a member joins
{ event: MemberAddedEvent, listener: sendWelcomeMessage },
],
})
export class OnboardingModule {}

Here is a full feature built with a module, with routes co-located inside the feature directory:

src/features/invoice/
├── invoice.service.ts
├── invoice.router.ts
├── invoice.module.ts
├── events/
│ └── invoice-created.event.ts
├── jobs/
│ └── generate-pdf.job-handler.ts
├── listeners/
│ └── send-invoice-email.listener.ts
├── routes/
│ ├── index.tsx # /invoices (list page)
│ └── $id.tsx # /invoices/:id (detail page)
└── index.ts
features/invoice/invoice.module.ts
import { Module } from '@cruzjs/core/di';
import { JOB_HANDLER } from '@cruzjs/core/jobs';
import { InvoiceService } from './invoice.service';
import { invoiceRouter } from './invoice.router';
import { InvoiceCreatedEvent } from './events';
import { sendInvoiceEmailListener } from './listeners/send-invoice-email.listener';
import { GeneratePdfJobHandler } from './jobs/generate-pdf.job-handler';
@Module({
providers: [
InvoiceService,
{ provide: JOB_HANDLER, useClass: GeneratePdfJobHandler, multi: true },
],
trpcRouters: {
invoice: invoiceRouter,
},
events: [
{ event: InvoiceCreatedEvent, listener: sendInvoiceEmailListener },
],
})
export class InvoiceModule {}

Routes are registered in src/routes.ts, pointing to the feature’s route files:

src/routes.ts
...prefix('invoices', [
index('features/invoice/routes/index.tsx'),
route(':id', 'features/invoice/routes/$id.tsx'),
]),
features/invoice/index.ts
export { InvoiceModule } from './invoice.module';
export { InvoiceService } from './invoice.service';

Legacy: Service Provider Pattern (Removed)

Section titled “Legacy: Service Provider Pattern (Removed)”

Before (removed):

// setup.server.ts — THIS FILE NO LONGER EXISTS
import { setUserProviders } from '@cruzjs/core/framework/application.server';
import { ProductProvider } from './features/product';
setUserProviders(() => [ProductProvider]);

After:

server.cloudflare.ts
import { createCruzApp } from '@cruzjs/core';
import { CloudflareAdapter } from '@cruzjs/adapter-cloudflare';
import * as schema from './database/schema';
import { ProductModule } from './features/product/product.module';
export default createCruzApp({
schema,
modules: [ProductModule],
adapter: new CloudflareAdapter(),
pages: () => import('virtual:react-router/server-build'),
});

No separate .provider.ts file is needed. The @Module class is the only file required.

PackagePurposeShould You Modify?
@cruzjs/coreFramework foundationNever
@cruzjs/proBilling, admin, audit loggingNever
@cruzjs/startUI components, themingNever
src/features/Your feature modules (including routes)Always
src/routes/Root-level pages and API routesAlways
src/components/Your shared UI componentsAlways

Extend the framework through modules and event listeners. Do not modify core or pro package source files.