Skip to content

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

A single function that receives a builder. Every method takes effect immediately:

ts
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:

ts
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

MethodWhat 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 reactivitydefinePlugin({ 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.

MIT licensed.