Creating NADIS Plugins
NADIS (Not A DI System) provides zero-boilerplate service injection for LumenizeDO. This guide shows you how to create your own plugins. For a real-world example, see @lumenize/fetch — the only NADIS plugin currently shipped with Lumenize.
The Three Steps
Creating a NADIS plugin requires three things:
- Extend
NadisPlugin— Your plugin class gets access tothis.doInstance,this.ctx, andthis.svc - Register with NADIS — Call
NadisPlugin.register()at module scope so side-effect imports work - Declaration merging — Augment
LumenizeServicessothis.svc.yourPluginis fully typed
Here's how @lumenize/fetch does all three (simplified):
// ...
export class Fetch extends NadisPlugin {
// ...
// Eager dependency validation - fails immediately if alarms not available
// ...
if (!this.svc.alarms) {
throw new Error('Fetch requires alarms service for timeout handling (should be built-in to @lumenize/mesh)');
}
// ...
}
// TypeScript declaration merging - augments LumenizeServices interface
// Provides type safety for this.svc.fetch
declare global {
interface LumenizeServices {
fetch: Fetch;
}
}
// Register fetch service using NadisPlugin helper
NadisPlugin.register('fetch', (doInstance) => new Fetch(doInstance));
And the package's index.ts uses a side-effect import to trigger registration:
// ...
// Main NADIS plugin (side-effect import registers it)
export { Fetch, type FetchMessage } from './fetch';
// ...
// Side-effect import to register the NADIS plugin
import './fetch';
Consumers then just import the package:
import '@lumenize/fetch'; // Side-effect registers this.svc.fetch
What NadisPlugin Provides
Extending NadisPlugin gives your plugin:
this.doInstance— Reference to theLumenizeDOinstancethis.ctx— Direct access toDurableObjectState(storage, alarms, etc.)this.svc— Access to other NADIS services (built-in and plugin)NadisPlugin.register()— Static helper for registration
Declaration Merging
The only required boilerplate is declaration merging for type safety:
// ...
declare global {
interface LumenizeServices {
fetch: Fetch;
}
}
// ...
This enables full IntelliSense and compile-time type checking for this.svc.fetch. It's required because TypeScript can't infer what properties exist on this.svc from runtime registration alone.
Guidelines
Eager Dependency Validation
If your plugin depends on other services, validate in the constructor so failures surface at instantiation time rather than when a method is first called. @lumenize/fetch demonstrates this — it checks for this.svc.alarms immediately and throws a clear error if missing.
Side-Effect Imports
The registration pattern works via side effects: the NadisPlugin.register() call executes when the module is imported. Your package's index.ts should both export the class and import the file for its side effect (as shown in the @lumenize/fetch example above). Consumers then use a bare import like import '@lumenize/my-plugin';.
Instance Variable Rules
DOs can be evicted from memory at any time. Never use instance variables for mutable application state — always store that in ctx.storage.
Instance variables are only safe for:
- Statically initialized utilities (e.g.,
#log = debug('MyPlugin')) - Ephemeral caches where storage is the source of truth
- Configuration set once in constructor (e.g.,
#ttl = options?.ttl ?? 3600)
How NADIS Works
NADIS uses three simple mechanisms:
- Service Registration: Packages register factory functions in a global registry on import
- Type Safety: TypeScript declaration merging provides autocomplete and type checking
- Lazy Resolution:
LumenizeDOuses a Proxy to lazily instantiate services when first accessed, then caches them
No decorators, no reflection, no complex DI containers.
sql and alarms are available on this.svc but are not NADIS plugins — they're built-in services registered directly in LumenizeDO, always available without any import.