Skip to content
Last updated Give Feedback

Hooks & Filters

Hooks let your plugin react to things that happen in ecommus. The host emits structured events; your plugin’s handler runs after the emit. Event names use dot notation (order.placed, not order:placed) — the canonical names live in packages/core/src/events.ts as the EVENTS constant.

Permission required: events.subscribe.

ctx.registerHook("order.paid", async (event) => {
const { orderId } = event.payload;
// Do something
});

Canonical event names (from packages/core/src/events.ts)

Section titled “Canonical event names (from packages/core/src/events.ts)”

These are the events the host actually emits. Treat the source-of-truth in EVENTS as authoritative — this table is a snapshot at session 44.

Event constantWire nameWhere it fires
USER_REGISTEREDuser.registeredapps/api/src/routes/auth.ts on signup
USER_LOGGED_INuser.logged_inapps/api/src/routes/auth.ts on login
CART_UPDATEDcart.updatedapps/api/src/services/cart.service.ts
CART_ABANDONEDcart.abandonedabandoned-cart worker
ORDER_PLACEDorder.placedorder.service.ts after order creation
ORDER_PAIDorder.paidwebhook flow after Stripe payment_intent.succeeded
ORDER_SHIPPEDorder.shippedorder.service.ts after shipping label issued
ORDER_DELIVEREDorder.deliveredshipping carrier webhook
ORDER_CANCELLEDorder.cancelledorder.service.ts
ORDER_REFUNDEDorder.refundedrefund flow
PAYMENT_SUCCEEDEDpayment.succeededpayment.service.ts
PAYMENT_FAILEDpayment.failedpayment.service.ts
INVENTORY_LOWinventory.lowinventory worker on stock below threshold
PRODUCT_VIEWEDproduct.viewedstorefront analytics emit
SEARCH_PERFORMEDsearch.performedsearch service

Subscribe to these by their wire name (order.placed, etc.):

ctx.registerHook("order.placed", async (event) => {
ctx.logger.info({ orderId: event.payload.orderId }, "order observed");
});

For custom plugin-to-plugin events, you can emit + listen on the raw bus:

import { bus } from "@ecommus/core";
bus.on("order.paid", (event) => {
/* ... */
});
// Custom events from your plugin (use a namespace prefix to avoid collisions)
bus.emit({
type: "@ecommus-plugin/my-feature.sync-complete",
payload: { tenantId, count: 42 },
occurredAt: new Date(),
});

The standard pattern is to use ctx.registerHook(...) instead — it gets you the per-plugin scoping and the permission check. Only drop down to bus when you need to emit (registry doesn’t proxy emits).


Filters let your plugin modify computed values before they are returned. Multiple plugins can chain filters — each receives the value returned by the previous filter.

Permission required: events.filter.

ctx.registerFilter<number>("product.price", async (price, context) => {
// Modify the price
return price * 0.9; // 10% discount
});

Filter status — registry-only, no built-in filter sites yet

Section titled “Filter status — registry-only, no built-in filter sites yet”

⚠️ Honest state of play (session 44): the registry accepts filter registrations and stores them, but the host services (pricing, tax, shipping, email, etc.) do not yet call registry.applyFilter(...) at filter sites. Your registerFilter calls succeed, but the handlers never fire because no code emits a filter request. The system is wired for plugins to participate; the host integration is a separate sprint.

This is documented in the backlog under “Filter site wiring (D-series)”. Until that lands, use hooks + read-only logic for derived values instead of filters.

Names we plan to wire (when we close the gap)

Section titled “Names we plan to wire (when we close the gap)”

These are the proposed filter names. Names use dot notation to match the events convention. The Input and Output columns are proposed shapes; treat them as draft.

FilterInputOutputNotes
product.pricenumbernumberFinal product price after base pricing
cart.totalCartTotalsCartTotalsCart total before checkout
order.shipping_costnumbernumberCalculated shipping cost
order.tax_amountTaxResultTaxResultTax calculation result
email.subjectstringstringEmail subject (per-template override)

Applying filters programmatically (host-side, when wired)

Section titled “Applying filters programmatically (host-side, when wired)”

Once the host integration lands, services apply filters like this:

import { registry } from "@ecommus/plugin-sdk";
const finalPrice = await registry.applyFilter(
"product.price",
basePrice,
context
);

Plugin authors don’t call applyFilter directly — they only register handlers. The host calls applyFilter at the right moment in its own flow.