Use getNestModules for webhooks, public probes, OAuth callbacks, or long-lived services. Use getAuthExcludedRoutes to skip UCAN auth on specific paths.
Write a NestJS controller and module
A plugin's module is a regular Nest module. Use a static register(opts) returning a DynamicModule if you want to pass config into DI.
import {
Controller,
type DynamicModule,
Get,
Inject,
Module,
Query,
} from '@nestjs/common';
import { getCurrentWeather, type Units } from './weather-client.js';
export const WEATHER_DEFAULT_UNITS = 'WEATHER_DEFAULT_UNITS';
@Controller('weather')
export class WeatherController {
constructor(@Inject(WEATHER_DEFAULT_UNITS) private readonly units: Units) {}
@Get('now')
async now(@Query('city') city?: string) {
if (!city) return { ok: false, error: 'Missing required query param: city' };
const result = await getCurrentWeather(city, this.units);
if (!result) return { ok: false, error: `Could not find weather for "${city}".` };
return { ok: true, ...result };
}
}
@Module({})
export class WeatherHttpModule {
static register(units: Units): DynamicModule {
return {
module: WeatherHttpModule,
controllers: [WeatherController],
providers: [{ provide: WEATHER_DEFAULT_UNITS, useValue: units }],
};
}
}Canonical source: weather.module.ts.
Return the module from getNestModules(ctx)
ctx is optional but useful for reading typed config when registering the module.
import { OraclePlugin, type PluginContext } from '@ixo/oracle-runtime';
import type { DynamicModule } from '@nestjs/common';
export class WeatherPlugin extends OraclePlugin {
override getNestModules(ctx: PluginContext): DynamicModule[] {
const units = configSchema.parse(ctx.config).WEATHER_DEFAULT_UNITS;
return [WeatherHttpModule.register(units)];
}
}Plain module classes also work: return [SlackModule]. The runtime spreads the returned modules into RuntimeAppModule.imports. See getNestModules in weather.plugin.ts.
Opt routes out of UCAN auth (optional)
By default every plugin route goes through AuthHeaderMiddleware. For webhooks, OAuth callbacks, or public probes, return them from getAuthExcludedRoutes.
import { RequestMethod } from '@nestjs/common';
import type { AuthExcludedRoute } from '@ixo/oracle-runtime';
override getAuthExcludedRoutes(): AuthExcludedRoute[] {
return [{ path: 'weather/now', method: RequestMethod.GET }];
}Match by the full path the controller mounts at (weather/now, not now). method defaults to RequestMethod.ALL. See getAuthExcludedRoutes in weather.plugin.ts.
Verify the endpoint
Boot the oracle with pnpm dev, then curl the route without an auth header.
curl 'http://localhost:3000/weather/now?city=Berlin'Other routes still 401 — the exclusion list only opts these specific paths out.
What to know before shipping
- The same hook handles long-lived services. Slack, BullMQ workers, polling clients all ship as Nest modules with
OnModuleInitlifecycle. - Plugin modules have full DI access to the runtime's services (Sessions, Messages, Secrets, UCAN, …).
- Host code can also opt routes out of auth via
createOracleApp({ authExcludedRoutes }). Plugin and host lists merge with built-in exclusions (/health,/docs). - Leading slash on
pathis optional. The matcher mirrors NestJS'sMiddlewareConsumer.exclude(...). - Plugin route paths are namespaced by the controller's
@Controller('weather')decorator — keep them under your plugin's name to avoid collisions.