Skip to content

Forge sub-plugins (à la carte)

The bundled forgePlugins() set is a convenience — one install, every piece in place. Sub-plugins exist for two reasons:

  1. Smaller surface. Apps that don't use actors don't need them; apps that don't issue queries don't need the runner.
  2. Swap a single piece. Subbing in a Redis-backed IdempotencyStore or a Postgres ProjectionStore is one option swap on the relevant sub-plugin.

Available sub-plugins

PluginWhat it ownsBound on container
actorsPlugin()actor state transitions on LocalDelivery@800forge.actorStore, forge.actorChain
projectionsPlugin()projection folds on LocalDelivery@600forge.projectionStore
queriesPlugin()query runner over projections + handlersforge.queryRunner
workflowsPlugin()workflow correlation + fire on LocalDelivery@400forge.workflowRunner
idempotencyPlugin()atomic dedup gate on LocalDelivery@1000forge.idempotencyStore
dlqPlugin()dead-letter sink for failed actionsforge.deadLetterSink
actionsPlugin()command pipeline + the shared publish pathforge.actionRunner

Each fold step attaches to the core runtime.hooks.LocalDelivery chain at its fixed priority slot. The order is structural — .with() install order doesn't change runtime order, only the priority does.

The priority chain

1000 — idempotency  (short-circuits on duplicate dedupKey)
 800 — actors       (state transitions)
 600 — projections  (read-model folds)
 400 — workflows    (correlation + fire)

The bus and outbound sinks are not chain steps — they're the outgoing leg of publish, applied after local delivery for .public() events. See the local delivery chain for the full mental model.

Two compositions

Bundle + override

The most common shape. Install the forgePlugins({...}) set with the store you want swapped — every option is an infrastructure seam (stores, bus, dead-letter sink); primitives are registered by the app, never passed to forge:

ts
import { createApp } from "@nwire/app";
import { forgePlugins } from "@nwire/forge";
import { RedisIdempotencyStore } from "./redis-idempotency-store";

const app = createApp({
  appName: "shop",
  handlers: [...handlers, ...queries],
  plugins: [...forgePlugins({ idempotencyStore: new RedisIdempotencyStore(client) })],
});

If you want to drop whole concerns instead of swapping a store, build à la carte (below).

À la carte

Build the chain from sub-plugins only. The bundled hook slots have to be materialised first — wrap that in a tiny local plugin:

ts
import { createApp, defineRegistry, type PluginDefinition } from "@nwire/app";
import {
  actorsPlugin,
  projectionsPlugin,
  queriesPlugin,
  actionsPlugin,
  idempotencyPlugin,
} from "@nwire/forge";

function forgeHookSlotsPlugin(): PluginDefinition {
  return {
    name: "forge.hook-slots",
    setup({ runtime }) {
      // Local event delivery rides the core `LocalDelivery` hook (always
      // present); only forge's Action*/Event* slots need materialising.
      for (const slot of [
        "ActionDispatching", "ActionCompleted", "ActionFailed",
        "EventRecording", "EventRecorded",
      ] as const) {
        runtime.defineHook(slot);
      }
    },
  };
}

const app = createApp({
  appName: "shop",
  registry: defineRegistry({ handlers: [placeOrder, listOrders], projections: [Dashboard] }),
})
  .with(forgeHookSlotsPlugin())
  .with(idempotencyPlugin())
  .with(projectionsPlugin())
  .with(queriesPlugin())
  .with(actionsPlugin());

Dispatch action via the action runner:

ts
import { FORGE_ACTION_RUNNER_BINDING } from "@nwire/forge";

const actions = app.container.resolve(FORGE_ACTION_RUNNER_BINDING);
await actions.dispatch(placeOrder, { orderId: "o-1", amount: 100 });

See the forge-subplugins example for the full pattern.

Custom stores

Each sub-plugin accepts an option to override its store:

Sub-pluginOption
actorsPlugin({ actorStore })ActorStore interface
projectionsPlugin({ projectionStore })ProjectionStore interface
idempotencyPlugin({ idempotencyStore })IdempotencyStore interface
dlqPlugin({ deadLetterSink })DeadLetterSink interface

The default for each is the in-memory implementation. Production deployments swap them for durable backends (Postgres, Redis, S3).

See also

MIT licensed.