Skip to content

Plugin authoring (definePlugin closure form)

A plugin is a closure that receives a builder and uses it to register bindings, listen for framework events, and run lifecycle hooks. The closure is the only API surface — no decorators, no class hierarchy.

The builder

ts
import { definePlugin } from "@nwire/app"

const myPlugin = definePlugin("my-plugin", ({
  provide,    // register a container binding
  on,         // listen for a framework event
  before,     // sugar for on("XAboutTo...", ..., mode: "series-bail")
  after,      // sugar for on("XWas...", ..., mode: "parallel")
  middleware, // mount onion-style middleware on the action pipeline
  actorHooks, // listen for actor lifecycle (loaded / saved / transitioned)
  boot,       // run once at startup, in declared order
  shutdown,   // run once at shutdown, in reverse boot order
}) => {
  // closure body — register everything you need
})

Anatomy of a real plugin

ts
import { token } from "@nwire/container"
import { definePlugin } from "@nwire/app"

const Db = token<{ query(sql: string): Promise<unknown[]>; close(): Promise<void> }>("Db")

export const dbPlugin = definePlugin("db", ({ provide, on, boot, shutdown }) => {
  let conn: Awaited<ReturnType<typeof openDb>>

  // 1. Provide a binding — but the value isn't ready yet.
  provide(Db, () => conn)

  // 2. Open the connection at boot.
  boot(async () => {
    conn = await openDb(process.env.DATABASE_URL!)
  })

  // 3. Close it at shutdown.
  shutdown(async () => {
    await conn.close()
  })

  // 4. Optional — react to lifecycle events.
  on("AppBooted", () => console.log("db plugin: connected"))
})

Ordering

  • Boot order is the order plugins are declared in createApp({ plugins }).
  • Shutdown order is the reverse of boot order.

If dbPlugin boots before migrationsPlugin, the database is connected first and migrations run second; on shutdown, migrations stop first and the connection closes last.

Middleware

middleware(fn) mounts a function on the action pipeline. The signature is onion-shaped — call next() to invoke the next layer, do work before/after.

ts
const loggingPlugin = definePlugin("logging", ({ middleware }) => {
  middleware(async (ctx, next) => {
    const t0 = Date.now()
    await next()
    console.log(`${ctx.action.name} took ${Date.now() - t0}ms`)
  })
})

Where examples live

  • packages/nwire-tracing/src/tracing-plugin.ts — production observability plugin.
  • packages/nwire-rbac/src/rbac-plugin.ts — authorization plugin (middleware + before-dispatch hook).
  • packages/nwire-storage/src/storage-plugin.ts — provides a contract; per-driver packages register implementations.

See also

MIT licensed.