WaClient without forking it. A plugin can hook into the incoming stanza pipeline, expose its own coordinator at client[exposeAs], contribute typed events to client.on, and clean up on disconnect — all with full TypeScript inference, no global module augmentation.
@zapo-js/voip is the reference implementation; see the VoIP guide for what a finished plugin looks like.
What a plugin is
Every plugin is aWaClientPluginDefinition value passed via the plugins option:
- Behavior plugins run side effects (register stanza handlers, listen to events, schedule work) and don’t expose anything on the client.
- Expose plugins also publish a value at
client[exposeAs]— typically a coordinator object, likeclient.voip.
client.on / client.once / client.off only when the plugin is in the plugins array. The inference is value-derived: if the plugin is not installed, the event name doesn’t exist on the client type.
Authoring a plugin
UsedefineWaClientPlugin for type-safe authoring. It has two overloads.
Behavior plugin
Expose plugin
The expose overload takes three type parameters: theexposeAs key as a string literal, the type of the value returned by setup, and the event-map type. Inference flows from the value back into client.on and client[exposeAs]:
metricsPlugin() into plugins, client.metrics is typed as Metrics, and client.on('metrics_tick', ...) is type-checked against MetricsEvents.
The
exposeAs parameter is constrained to K extends keyof WaClient ? never : K. Trying to expose a name that already exists on the client — 'message', 'group', 'voip' if voip is also installed — is a TypeScript error at the call site.Plugin context
setup receives a WaClientPluginContext:
Incoming handlers
registerIncomingHandler({ tag, prepend, handler }) lets you intercept a specific stanza tag ('message', 'iq', 'call', 'ack', 'receipt', …). Return true from handler to signal you handled the stanza (the core client won’t ack again); return false to let the rest of the pipeline run.
registerIncomingStanzaFilter runs even earlier and can drop stanzas entirely.
Typed event extension
Plugins thread their events into the host client through a phantom__pluginEvents marker carried by the return value of defineWaClientPlugin. The third generic parameter is the event map:
WaClient’s event type is derived from the plugins tuple via WaClientPluginEventsFromPlugins. The practical effect: a voip_* event exists on client.on only when voipPlugin() is in plugins.
WaClient in the same project doesn’t inherit foreign plugin events.
Lifecycle
Plugins install duringnew WaClient(...):
- The client is constructed and its coordinators wired.
installWaClientPluginsiterates thepluginsarray in order. For each plugin:- The
idis checked against previously seen ids. - If
exposeAsis set, the name is checked against other plugins and against existing members ofWaClient. setup(ctx)runs. The return value (for expose plugins) is published as a non-configurable, non-writable getter on the client.
- The
- On
client.disconnect(), after incoming handlers drain, the registereddisposecallbacks run in reverse (LIFO) order. A throwing dispose is logged and the rest still run. - On the next
client.connect(), plugins are reinstalled from scratch — the previousdisconnect()disposed them, soconnect()runssetup(ctx)again.client[exposeAs]accessors keep working across a reconnect (they resolve against the freshly-installed coordinator instance), andsetupsees a clean context every time.
Error conditions
installWaClientPlugins throws synchronously during client construction in these cases:
- Duplicate
id—"duplicate wa client plugin id: <id>". Each plugin must have a unique id across thepluginsarray. - Duplicate
exposeAs—"duplicate wa client plugin exposeAs: <name>". Two expose plugins cannot publish the same name. - Collision with a built-in —
"wa client plugin exposeAs \"<name>\" collides with a reserved client member". TryingexposeAs: 'message','group','on', etc. fails at runtime (and at compile time via thekeyof WaClientguard).
See also
VoIP guide
Place and receive WhatsApp calls via
@zapo-js/voip, the reference plugin.Configuration
Where
plugins sits on WaClientOptions.