> ## Documentation Index
> Fetch the complete documentation index at: https://ship.paralect.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Mutation events

> Every write to the database emits a typed MutationEvent. React to inserts, updates and deletes without coupling side effects to the endpoint that caused them.

In Ship, **every write to the database emits an event**. Insert a row, update one, delete one — the typed `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:

```ts theme={null}
import type { MutationEvent } from '@ship/db';

interface MutationEvent<T> {
  type: 'insert' | 'update' | 'delete';
  docs: T[];        // the rows that were written
  prevDocs?: T[];   // the rows as they were before (update / delete only)
}
```

A single event carries the full set of affected rows — `insertMany`, `updateMany` and `deleteMany` emit one event with every row in `docs`, so a handler always iterates.

## What each type carries

<Tabs>
  <Tab title="insert">
    `insertOne` / `insertMany` emit `type: 'insert'`. `docs` holds the new rows; there is no `prevDocs`.

    ```ts theme={null}
    { type: 'insert', docs: [user] }
    ```
  </Tab>

  <Tab title="update">
    `updateOne` / `updateMany` emit `type: 'update'`. `docs` holds the rows after the write, `prevDocs` the same rows before it — diff them to see exactly what changed.

    ```ts theme={null}
    { type: 'update', docs: [user], prevDocs: [userBefore] }
    ```
  </Tab>

  <Tab title="delete">
    `deleteOne` / `deleteMany` emit `type: 'delete'`. `docs` holds the rows that were removed.

    ```ts theme={null}
    { type: 'delete', docs: [user] }
    ```
  </Tab>
</Tabs>

<Note>
  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'`.
</Note>

## 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:

```ts theme={null}
eventBus.on('users.update', ({ docs, prevDocs }) => {
  for (const [i, user] of docs.entries()) {
    const before = prevDocs?.[i];
    if (before && before.email !== user.email) {
      logger.info(`User ${user.id} email changed: ${before.email} → ${user.email}`);
    }
  }
});
```

<Info>
  `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.
</Info>

## 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 |

The keys are typed — `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](https://stripe.com/docs/api/events/types) in their system.

## Next steps

* Write a handler in [Event handlers](/docs/api-reference/event-handler).
* See the decoupling patterns in [Using events](/docs/api-reference/using-events).
