DbService publishes a MutationEvent describing exactly what changed. Handlers subscribe to those events and run side effects: track analytics, push over a socket, denormalise a counter, fire a webhook. The endpoint stays a thin write; the reactions live elsewhere.
This is the seam that keeps resources decoupled. The endpoint that updates a user doesn’t know — or care — that a socket handler broadcasts the change to the browser.
The event shape
Mutation events are emitted by@ship/db’s DbService and typed against the table they came from:
insertMany, updateMany and deleteMany emit one event with every row in docs, so a handler always iterates.
What each type carries
- insert
- update
- delete
insertOne / insertMany emit type: 'insert'. docs holds the new rows; there is no prevDocs.Ship tables use soft deletes by default —
deletedAt lives on baseColumns, so a “delete” in your domain is usually an updateOne(..., { deletedAt: new Date() }), which emits type: 'update'. A hard deleteOne / deleteMany is what emits type: 'delete'.Comparing before and after
For updates,prevDocs is the previous version of each row, in the same order as docs. That makes “did this field actually change?” a one-liner — no diff library, no change object:
prevDocs is only loaded when a table actually has handlers wired up — DbService reads the previous rows before an update or delete, then skips that read when nothing is listening. You don’t pay for events you don’t use.Event keys
Each event is published under a key of`${table}.${type}`, so handlers subscribe to a specific table-and-operation pair:
| Key | Fires when |
|---|---|
users.insert | a row is inserted into users |
users.update | a row in users is updated |
users.delete | a row in users is hard-deleted |
eventBus.on only accepts `${TableName}.${MutationType}`, so a typo or a non-existent operation won’t compile.
Why events
Events solve two problems cleanly:- Cross-resource side effects without coupling. Keep a change history, broadcast over a socket, or sync to an external system by subscribing to a resource’s events — not by importing its service into yours. The resource that owns the write stays unaware of who reacts.
- One place that describes system behaviour. Every meaningful change flows through the same typed channel. Reading the
handlers/folders tells you everything that happens as a consequence of a write — much like Stripe exposing an event for every change in their system.
Next steps
- Write a handler in Event handlers.
- See the decoupling patterns in Using events.
