Skip to main content

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:

  1. Extend NadisPlugin — Your plugin class gets access to this.doInstance, this.ctx, and this.svc
  2. Register with NADIS — Call NadisPlugin.register() at module scope so side-effect imports work
  3. Declaration merging — Augment LumenizeServices so this.svc.yourPlugin is 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 the LumenizeDO instance
  • this.ctx — Direct access to DurableObjectState (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:

  1. Service Registration: Packages register factory functions in a global registry on import
  2. Type Safety: TypeScript declaration merging provides autocomplete and type checking
  3. Lazy Resolution: LumenizeDO uses a Proxy to lazily instantiate services when first accessed, then caches them

No decorators, no reflection, no complex DI containers.

Built-in Services

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.