Skip to content

Capability

A typed slice of ctx. A capability declares what it adds to every handler's ctx at dispatch time, and carries that contribution forward in the App's type so handler authors see exactly what they have.

Capabilities are how every plugin contributes to handler ctx without the App layer or the runtime knowing the specifics. execute / send / publish / useProjection (forge), currentUser (auth), enqueue (queues) — all are capabilities.

Shape

ts
import { defineCapability, type Capability } from "@nwire/runtime";

export interface ClockCaps {
  now(): Date;
}

export const clockCap: Capability<ClockCaps> = defineCapability<ClockCaps>({
  name: "clock",
  provideCtx: () => ({
    now: () => new Date(),
  }),
});

Two pieces:

  • provideCtx({envelope, container, runtime}) — called once per dispatch. Returns an object whose members are spread into the handler's ctx.
  • __mark: TMark — phantom type. Never read at runtime. Carries the ctx-shape into App<TCaps> via structural intersection.

Installing one

Capabilities are installed by plugins, not by hand. Wrap with definePlugin:

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

export function clockPlugin() {
  return definePlugin<ClockCaps>("clock", {
    setup: ({ add }) => {
      add(clockCap);
    },
  });
}

Then layer it into the App:

ts
const app = createApp({ appName: "shop" })
  .with(clockPlugin())
  .with(idPlugin());

App<TCaps>'s TCaps becomes ClockCaps & IdCaps. A handler typed (input, ctx: ClockCaps & IdCaps) will compile here; one that asks for ClockCaps & IdCaps & DbCaps won't, because the App doesn't carry DbCaps.

Per-dispatch lifecycle

provideCtx runs once per dispatch, not once per process. Closure state set up inside it is per-dispatch:

ts
defineCapability<{ newId(): string }>({
  name: "id",
  provideCtx: () => {
    let seq = 0;
    return {
      newId: () => `id-${++seq}`,
    };
  },
});

Two dispatches each see id-1 on their first call — the closure isn't shared. Use the container or runtime if you need shared state across dispatches.

Provide a runtime-level method

A capability can also contribute a method to runtime itself, callable without dispatching:

ts
defineCapability<{ now(): Date }>({
  name: "clock",
  provideCtx: () => ({ now: () => new Date() }),
  provideRuntime: (runtime) => ({
    now: () => new Date(),
  }),
});

After install, app.runtime.now() works alongside ctx.now() inside handlers. Useful for diagnostics, dev tooling, and bootstrap-time schedulers.

Why this matters

The capability primitive is the seam between the App layer and the plugin layer. Plugins talk only to capabilities; the App never names forge or queues or auth directly. Swap implementations by swapping plugins — the type system carries the ctx contract along for free.

See the capability-composition example for a runnable demo.

MIT licensed.