Skip to main content

Module overrides

Use entry.overrides in apps/<app>/src/modules.ts when an app needs to replace or disable a contract shipped by an upstream module. The override is static app configuration: it applies during bootstrap before the affected registries are stored.

import type { ModuleEntry } from './modules'

export const enabledModules: ModuleEntry[] = [
{
id: 'example',
from: '@app',
overrides: {
routes: {
api: {
'GET /api/example/override-probe': {
handler: async () => Response.json({ ok: true, source: 'override' }),
metadata: { requireAuth: false },
},
},
pages: {
'/backend/example': null,
'/frontend/store': { metadata: { title: 'Storefront' } },
},
},
},
},
]

Resolution order is:

  1. Programmatic apply*Overrides(...).
  2. modules.ts inline entry.overrides.
  3. File-based overrides where a domain supports them.
  4. The module's own registrations.

null disables the contract. A definition replaces it. Programmatic overrides can restore a contract disabled by modules.ts by mapping the same id back to a definition.

The default app and create-saasframe-app template also export a non-applied moduleOverrideExamples object from src/modules.ts. It is a copyable catalog for all wired domains; it does not change runtime behavior unless you assign one of those shapes to a module entry's overrides field.

Domains​

DomainKeyStable id
AIai.agents, ai.toolsagent id / tool name
API routesroutes.api'METHOD /api/path'
Page routesroutes.pages'/backend/path' or '/frontend/path'
Event subscribersevents.subscriberssubscriber id
Workersworkersworker id
Widget injectionwidgets.injectioninjection widget id
Component overrideswidgets.componentscomponent handle
Dashboard widgetswidgets.dashboardwidget id
Notification types and handlersnotifications.types, notifications.handlerstype id / handler id
API interceptorsinterceptorsinterceptor id
Command interceptorscommandInterceptorsinterceptor id
Response enrichersenrichersenricher id
Page guardsguardsmiddleware id
CLI commandsclicommand string
Setup hookssetupmodule id
ACL featuresacl.featuresfeature id
DI bindingsdicontainer key
Encryption mapsencryption.mapsentity id

Examples​

Full modules.ts Catalog​

Use this shape when you need to find the correct key for a domain quickly. Keep only the domains you actually intend to override.

import type { ModuleOverrides } from '@saasframe/shared/modules/overrides'

export const moduleOverrideExamples: ModuleOverrides = {
ai: {
agents: { 'catalog.catalog_assistant': null },
tools: { inbox_ops_accept_action: null },
},
routes: {
api: { 'DELETE /api/example/items': null },
pages: { '/backend/example/reports': null },
},
events: {
subscribers: { 'example.todo.audit': null },
},
workers: { 'example:sync': null },
widgets: {
injection: { 'example.sidebar': null },
components: { 'page:/backend/example': null },
dashboard: { 'example.kpi': null },
},
notifications: {
types: { 'example.notice': null },
handlers: { 'example.notice.toast': null },
},
interceptors: { 'example.items.interceptor': null },
commandInterceptors: { 'example.command.interceptor': null },
enrichers: { 'example.items.enricher': null },
guards: { 'example.backend.guard': null },
cli: { 'example seed': null },
setup: { seedExamples: false },
acl: {
features: { 'example.manage': null },
},
di: { exampleService: null },
encryption: {
maps: { 'example:item': null },
},
}

setup overrides apply to the module entry that carries them. For example, setup: { seedExamples: false } on { id: 'catalog', ... } skips catalog's seedExamples hook; it does not address setup hooks by a separate map key.

Routes​

overrides: {
routes: {
api: {
'DELETE /api/catalog/products/[id]': null,
'POST /api/catalog/products': {
handler: async (req) => Response.json({ ok: true }),
metadata: { requireAuth: true, requireFeatures: ['catalog.manage'] },
},
},
pages: {
'/backend/catalog/products': null,
'/frontend/products': { metadata: { navHidden: true } },
},
},
}

Events, Workers, CLI​

overrides: {
events: {
subscribers: {
'catalog.product.updated.reindex': null,
},
},
workers: {
'catalog:reindex': {
id: 'catalog:reindex',
queue: 'catalog-reindex-fast',
concurrency: 4,
handler: async (job, ctx) => {},
},
},
cli: {
'catalog reindex': null,
},
}

Widgets​

overrides: {
widgets: {
injection: {
'catalog.product.toolbar': null,
},
components: {
'page:/backend/catalog/products': {
target: { componentId: 'page:/backend/catalog/products' },
priority: 10,
propsTransform: (props) => props,
},
},
dashboard: {
'catalog.low_stock': null,
},
},
}

Notifications And Runtime Hooks​

overrides: {
notifications: {
types: {
'catalog.low_stock': null,
},
handlers: {
'catalog.low_stock.toast': null,
},
},
interceptors: {
'catalog.products.audit': null,
},
commandInterceptors: {
'catalog.products.command_audit': null,
},
enrichers: {
'catalog.products.stock_enricher': null,
},
guards: {
'catalog.products.page_guard': null,
},
}

Setup, ACL, DI, Encryption​

overrides: {
setup: {
defaultRoleFeatures: {
admin: ['catalog.view'],
},
seedExamples: false,
},
acl: {
features: {
'catalog.manage': null,
},
},
di: {
catalogPricingService: {
register: (container, key) => container.register({ [key]: { mode: 'custom' } }),
},
},
encryption: {
maps: {
'catalog:product': null,
},
},
}

Programmatic Overrides​

Use programmatic helpers for env-driven boot decisions and tests:

import {
applyApiRouteOverrides,
applyPageRouteOverrides,
applyWorkerOverrides,
} from '@saasframe/shared/modules/overrides'

applyApiRouteOverrides({ 'GET /api/example/override-probe': null })
applyPageRouteOverrides({ '/backend/example': null })
applyWorkerOverrides({ 'example:sync': null })

Programmatic calls must happen before the affected registry registers its base entries. The standard app bootstrap already calls applyModuleOverridesFromEnabledModules(enabledModules) for inline modules.ts overrides.

Notes​

  • Override your own app module by editing the module directly; use entry.overrides for cross-module app policy.
  • API route keys normalize method case and trailing slashes.
  • Page route keys normalize /frontend/foo to frontend route /foo; /backend/foo stays backend.
  • Stale keys log a warning so operators can find renamed or removed upstream contracts.