definePlugin
Cross-cutting framework extension. Modules describe domain; plugins span the system.
A plugin can register container bindings, subscribe to framework or domain events, add dispatch middleware, hook actor lifecycle, and run boot/shutdown work — all from one declaration.
Two forms
Closure form (recommended)
A single function that receives a builder. Every method takes effect immediately:
import { definePlugin, ActionDispatching } from "@nwire/forge"
export const auditPlugin = definePlugin("audit", ({
bind, resolve, on, before, after, middleware, boot, shutdown,
}) => {
bind("auditLog", () => new AuditLog({ topic: "actions" }))
before("post.publish", async ({ ctx }) => {
if (!ctx.envelope.user) return false // veto anonymous publishes
})
after("post.publish", async ({ action, result }) => {
resolve<AuditLog>("auditLog").record(action.name, result)
})
middleware(async (next, action, _input, ctx) => {
const start = performance.now()
try { return await next() }
finally {
log.info(`${action.name}`, { ms: performance.now() - start })
}
})
boot(async () => {
await resolve<AuditLog>("auditLog").connect()
})
shutdown(async () => {
await resolve<AuditLog>("auditLog").flush()
})
})Object form (back-compat)
The legacy shape still works — useful when you need explicit lifecycle phases and don't want the closure:
export const audit = definePlugin("audit", {
register: (container) => {
container.register("auditLog", () => new AuditLog())
},
boot: async (container) => {
await container.resolve<AuditLog>("auditLog").connect()
},
shutdown: async (container) => {
await container.resolve<AuditLog>("auditLog").flush()
},
middleware: [/* ... */],
})Builder API
| Method | What it does |
|---|---|
bind(name, factory) | Register a lazy singleton on the container |
resolve<T>(name) | Look up a binding (provider, plugin bind) |
on(Event, handler, prio?) | Subscribe to any framework event |
before(actionName, handler) | Sugar: on(ActionDispatching, filter-by-name). Return false to veto |
after(actionName, handler) | Sugar: on(ActionCompleted, filter-by-name). Observe success |
middleware(mw) | Add a dispatch middleware (onion-style; first call is outermost) |
actorHook(hook) | Subscribe to every actor transition |
boot(fn) | Async startup work — runs after providers booted |
shutdown(fn) | Async teardown — runs in REVERSE registration order |
The closure runs synchronously during plugin register. Use boot(...) for any async work.
When to use a plugin (vs other primitives)
| You want to… | Use |
|---|---|
| Add domain behavior (handler, projection, query) | A module |
| Provide a bootable dependency (DB pool, Redis client) | definePlugin({ provide, boot }) |
| Add cross-cutting behavior (tracing, audit, authz) | definePlugin({ middleware }) |
| Run code only at startup, no ongoing reactivity | definePlugin({ boot }) |
Plugins are listed in createApp({ plugins: [...] }). Order matters: register/setup run in declaration order; shutdown runs in reverse.
Composition
Plugins compose freely — auth + rbac + audit + observability all coexist, each subscribing to whatever framework events it cares about. The runtime ensures their subscriptions don't trample each other; priority lets you order policy checks ahead of observers when needed.
See framework-events for the catalog of events you can subscribe to.