Skip to content

Module setup ​

How your app hands Kavo its infrastructure and app-wide options: KavoModule.forRoot/forRootAsync, and building KavoContext.app from the request.

Global config (KavoModule.forRoot / forRootAsync) ​

ts
KavoModule.forRootAsync({
  useFactory: () => ({
    infrastructure: createInfrastructure(dataSource),
    defaults: {/* KavoSettings, see Settings */},
    paginationStrategies: [],
  }),
  provideServices: true,
  graphql: true, // unguarded: add { guards: [...] } before exposing it (see below)
});
FieldTypeWhat it does
infrastructureKavoInfrastructureWhere entity metadata and the repository adapter come from: createInfrastructure(dataSource) or createInfrastructure(client, opts). Required for any @Kavo route to run.
defaultsDeepPartial<KavoSettings>App-wide settings, one level below the built-in defaults and above every entity's own config. See Settings for what's in KavoSettings.
paginationStrategiesreadonly PaginationStrategy[]Registers custom pagination strategies beyond the built-in "offset", so pagination.strategy can name one of these instead.
realtimeTransportsreadonly RealtimeTransport[]Registers the transports (e.g. createTransport(...) from @kavo/sse) that every entity's write events publish to. This is process-wide, not per entity (ADR-0023). An entity still needs its own realtime: { events: {...} } (any object; see realtime) before its writes publish anything. Registering a transport alone does not turn realtime on.
cacheStoreCacheStoreThe store every entity's cache reads and writes. Process-wide and shared by every entity, not per entity (ADR-0031). Defaults to an in-process createMemoryCacheStore(); hand one instance to every root for a process-wide cache, or implement get/set/invalidate over a shared backend. Registering a store alone does not turn caching on; an entity still needs its own cache: { ttl: ... } (see Result cache).
app(request) => KavoAppContextBuilds the application's request-scoped context every generated route puts on KavoContext.app — e.g. (request) => request.user as KavoAppContext. Unset leaves KavoContext.app as {}. See Wiring your own auth.
useFactory(...args) => KavoModuleOptions(forRootAsync only) Builds the options object, e.g. after awaiting dataSource.initialize().
injectreadonly (string | symbol | Type)[](forRootAsync only) DI tokens injected as useFactory's arguments.
provideServicesbooleanAlso provides getKavoServiceToken(Entity) as a real DI provider for every @Kavo-decorated class the process has seen. Needed only if some other class constructor-injects a @Kavo entity's service directly.
graphqlboolean | { path?: string; guards?: readonly (Type<CanActivate> | CanActivate)[] }Mounts a default GraphQL controller merging every entity that called registerKavoGraphQLTypes onto one schema, mutations included. true mounts at POST /graphql; { path } mounts elsewhere; { guards } puts those guards on it. Implies provideServices. Unguarded unless you pass guards: a guard on an entity's REST controller does not extend to this route. See Guarding the zero-config routes.
mcpboolean | { path?: string; guards?: readonly (Type<CanActivate> | CanActivate)[] }Mounts a default MCP controller (Streamable HTTP, stateless) exposing every @Kavo entity's full standard toolset, writes included, with no per-entity opt-in. true mounts at POST /mcp; { path } mounts elsewhere; { guards } puts those guards on it. Implies provideServices. Requires @modelcontextprotocol/sdk installed. Unguarded unless you pass guards: a guard on an entity's REST controller does not extend to this route. See Guarding the zero-config routes.

See Wiring your own auth for how app builds KavoContext.app from the request, and how to type it.

Guarding the zero-config routes ​

graphql and mcp each mount one route of their own, outside every @Kavo controller. A guard on an entity's REST controller never reaches them, so as true they answer anyone who can reach the path, writes included. Pass guards to protect them:

ts
KavoModule.forRootAsync({
  imports: [AuthModule],
  useFactory: () => ({ infrastructure: createInfrastructure(dataSource) }),
  graphql: { guards: [JwtAuthGuard] },
  mcp: { path: "api/mcp", guards: [ApiKeyGuard] },
});

The guards go on the generated controller class with @UseGuards, so they behave like any class-level guard: a denial stops the request before a resolver or tool runs, with the guard's own error (403 when it returns false, a 401 if it throws UnauthorizedException). A guard can be a class or a ready instance. Nest builds a guard class inside KavoModule, so its dependencies must come from a global module or from a module in forRootAsync's imports (AuthModule above). An app-wide APP_GUARD covers these routes too, with no guards option needed.

Keep these guards, and whatever they inject, singleton-scoped. A request-scoped guard, or one that injects REQUEST, makes Nest rebuild the controller per request, and the rebuilt controller never collects its toolset or schema. It fails closed rather than open: an allowed request finds no MCP tools, and GraphQL answers 500. The same applies to a request-scoped APP_GUARD. Read the request from the ExecutionContext that canActivate receives instead.

If the route needs more than a guard, such as a different method or transport, leave the option unset and write your own controller extending BaseKavoGraphQLController or BaseKavoMcpController.

See Security guide for the other routes and settings to check before production: @kavo/sse's route, body-size and rate limits, and the config keys that widen what a client reaches.