Hooks & Filters
Hooks & Filters
Section titled “Hooks & Filters”Hooks (Domain Events)
Section titled “Hooks (Domain Events)”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.
Registering a Hook
Section titled “Registering a Hook”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 constant | Wire name | Where it fires |
|---|---|---|
USER_REGISTERED | user.registered | apps/api/src/routes/auth.ts on signup |
USER_LOGGED_IN | user.logged_in | apps/api/src/routes/auth.ts on login |
CART_UPDATED | cart.updated | apps/api/src/services/cart.service.ts |
CART_ABANDONED | cart.abandoned | abandoned-cart worker |
ORDER_PLACED | order.placed | order.service.ts after order creation |
ORDER_PAID | order.paid | webhook flow after Stripe payment_intent.succeeded |
ORDER_SHIPPED | order.shipped | order.service.ts after shipping label issued |
ORDER_DELIVERED | order.delivered | shipping carrier webhook |
ORDER_CANCELLED | order.cancelled | order.service.ts |
ORDER_REFUNDED | order.refunded | refund flow |
PAYMENT_SUCCEEDED | payment.succeeded | payment.service.ts |
PAYMENT_FAILED | payment.failed | payment.service.ts |
INVENTORY_LOW | inventory.low | inventory worker on stock below threshold |
PRODUCT_VIEWED | product.viewed | storefront analytics emit |
SEARCH_PERFORMED | search.performed | search 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");});EventBus direct access
Section titled “EventBus direct access”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
Section titled “Filters”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.
Registering a Filter
Section titled “Registering a 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. YourregisterFiltercalls 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.
| Filter | Input | Output | Notes |
|---|---|---|---|
product.price | number | number | Final product price after base pricing |
cart.total | CartTotals | CartTotals | Cart total before checkout |
order.shipping_cost | number | number | Calculated shipping cost |
order.tax_amount | TaxResult | TaxResult | Tax calculation result |
email.subject | string | string | Email 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.