Typed Config & Services
Give a plugin a typed install config, and expose typed services that only its declared dependents can read.
Two module augmentation interfaces make a plugin's config and exposed services visible to TypeScript across your whole project: IPluginConfigMap and IPluginServiceMap. Both live in @codexa/core/http, and both are keyed by plugin name.
declare module '@codexa/core/http' {
interface IPluginConfigMap {
auth: { issuer: string };
}
interface IPluginServiceMap {
auth: {
verifyToken(token: string): Promise<{ userId: string } | null>;
};
}
}Once declared, install(authPlugin, config) requires a config matching { issuer: string }, and setup(scope, config) receives it typed the same way, with no manual generic arguments anywhere.
Exposing a service
Inside setup, call scope.exposeService(name, implementation). The implementation must match the shape declared for that service name in IPluginServiceMap.
import { definePlugin } from '@codexa/core/http';
export const authPlugin = definePlugin({
name: 'auth',
setup(scope, config) {
scope.exposeService('verifyToken', async (token) => {
if (token === '') return null;
return { userId: `${config.issuer}:${token}` };
});
},
});Reading a service from another plugin
A plugin can only read services from a plugin it declares in dependsOn. This is checked at the type level, getService only accepts a plugin name that appears in dependsOn, and at runtime, calling it for an undeclared dependency throws.
export const profilePlugin = definePlugin({
name: 'profile',
dependsOn: ['auth'] as const,
setup(scope) {
const verifyToken = scope.getService('auth', 'verifyToken');
scope.route({
method: 'GET',
path: '/me',
handler: async (ctx) => {
const token = ctx.headers.get('authorization')?.replace('Bearer ', '') ?? '';
const session = await verifyToken(token);
if (!session) {
return ctx.json({ error: 'Unauthorized' }, { status: 401 });
}
return ctx.json(session);
},
options: { name: 'profile.me', tags: ['guarded:profile'] },
});
},
});getServices('auth') returns every service auth has exposed, typed as the full IPluginServiceMap['auth'] shape, useful when a plugin depends on several services from the same dependency.
Checking without throwing
A few methods let a plugin check availability before committing to a call.
scope.hasDependency('auth'); // declared in dependsOn, and currently installed
scope.hasPlugin('auth'); // installed, regardless of dependsOn
scope.hasService('auth', 'verifyToken'); // declared as a dependency, and the service exists
scope.getDependencyNames(); // every name listed in this plugin's dependsOnReading services from the app itself
After installation, the root app can also read a plugin's exposed services, without any dependsOn restriction, since the app is not a plugin.
const app = createApp()
.install(authPlugin, { issuer: 'codexa' })
.install(profilePlugin);
const authServices = app.getServices('auth');
const verifyToken = app.getService('auth', 'verifyToken');dependsOn only controls service access. It does not give a plugin access to another plugin's routes, middleware, or internal state. The only way to share behavior across plugins is through explicitly exposed services.