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

# Event handlers

> A handler is a function that subscribes to a resource's mutation events and runs a side effect. Drop one in handlers/ — codegen wires it up.

A **handler** subscribes to a resource's [mutation events](/docs/api-reference/events) and runs a side effect when the data changes. Handlers live next to the resource that owns the data, in `apps/api/src/resources/<name>/handlers/*.ts`. Each file calls `eventBus.on(...)` at module scope — importing the file registers the subscription.

```
resources/users/
  handlers/
    sync-analytics.ts   # users.insert → track in analytics
    to-sockets.ts       # users.update → push to the browser
```

## Anatomy of a handler

A handler file imports `eventBus`, subscribes to one `` `${table}.${type}` `` key, iterates `docs`, and wraps the work in a `try/catch` so a side-effect failure never takes down the request that triggered it:

```ts theme={null}
import { eventBus } from '@/event-bus';
import logger from '@/logger';
import { analyticsService } from '@/services';

eventBus.on('users.insert', (data) => {
  try {
    for (const user of data.docs) {
      analyticsService.track('New user created', {
        fullName: user.fullName,
      });
    }
  } catch (err) {
    logger.error(`users.insert handler error: ${err}`);
  }
});
```

`data` is the typed [`MutationEvent`](/docs/api-reference/events) for the `users` table, so `user` is a fully-typed `User` row — `user.fullName`, `user.email` and `user.id` are all known to the compiler.

## Reacting to updates

`users.update` events carry both the new rows (`docs`) and the rows as they were (`prevDocs`). Here Ship pushes every updated user to their own browser over Socket.IO:

```ts theme={null}
import { eventBus } from '@/event-bus';
import ioEmitter from '@/io-emitter';
import logger from '@/logger';

eventBus.on('users.update', (data) => {
  try {
    for (const user of data.docs) {
      logger.debug(`Emitting user:updated to user ${user.id} (${user.email})`);
      ioEmitter.publishToUser(user.id, 'user:updated', user);
    }
  } catch (err) {
    logger.error(`users.update handler error: ${err}`);
  }
});
```

Need to act only when a specific field changed? Compare against `prevDocs` (same order as `docs`):

```ts theme={null}
eventBus.on('users.update', ({ docs, prevDocs }) => {
  for (const [i, user] of docs.entries()) {
    const before = prevDocs?.[i];
    if (before && !before.isEmailVerified && user.isEmailVerified) {
      analyticsService.track('Email verified', { userId: user.id });
    }
  }
});
```

## Opt a table into events

A table only emits events when its `DbService` is constructed with the event-bus hook. This is wired in the generated `src/db.ts`:

```ts theme={null}
users: new DbService<typeof users>(users, db, 'users', eventBus.hook('users')),
```

`eventBus.hook('users')` returns the `onMutation` callback `DbService` calls after every write — it re-emits the mutation as `users.insert` / `users.update` / `users.delete`. A table created **without** the fourth argument (e.g. `sessions`, `verifications`) writes silently and fires no events. Add the hook when you want a table to be observable.

<Note>
  `src/db.ts` is generated by `codegen-db.ts`. Re-run `pnpm --filter api codegen` after adding a schema; then add `eventBus.hook('<table>')` to that table's `DbService` if it should emit events.
</Note>

## Registration is automatic

You never wire handlers up by hand. `codegen-router.ts` scans every `resources/<name>/handlers/` folder and emits a side-effect import at the top of the generated `src/router.ts`:

```ts theme={null}
// Auto-generated by codegen-router.ts — do not edit manually

import './resources/users/handlers/sync-analytics';
import './resources/users/handlers/to-sockets';
```

Importing the module runs its top-level `eventBus.on(...)`, registering the subscription. So the workflow is:

<Steps>
  <Step title="Add the file">
    Create `resources/<name>/handlers/<what-it-does>.ts` and call `eventBus.on('<table>.<type>', ...)`.
  </Step>

  <Step title="Run codegen">
    `pnpm --filter api codegen` regenerates `router.ts` with the new side-effect import.
  </Step>

  <Step title="Done">
    The handler is live — no registry, no manual import.
  </Step>
</Steps>

<Tip>
  Name handlers for the effect, not the event: `sync-analytics.ts`, `to-sockets.ts`, `denormalise-counts.ts`. One file per side effect keeps each reaction independent and easy for an agent to find.
</Tip>

## Keep handlers safe

* **Always `try/catch`.** A handler runs in the same process as the write; an unguarded throw shouldn't surface as a request error. Log and move on.
* **Iterate `docs`.** Bulk mutations (`insertMany`, `updateMany`, `deleteMany`) deliver one event with many rows.
* **Stay idempotent where you can.** Treat handlers as best-effort reactions, not part of the transaction.

## Next steps

* The event shape and types: [Mutation events](/docs/api-reference/events).
* Cross-resource patterns: [Using events](/docs/api-reference/using-events).
