Skip to main content
Events exist to decouple. A resource owns its writes; anything that should happen as a consequence of a write lives in a handler that subscribes to the resource’s mutation events. The resource doing the write never imports the code that reacts to it — so features stay independent and easy for you, and your agents, to add or remove. Three patterns cover almost everything you’ll do with events.

Cross-resource side effects

When a change in one resource needs to update another, subscribe instead of reaching in. Say you keep a notesCount on each user so a profile page doesn’t have to aggregate. Rather than incrementing the counter inside every notes endpoint, the users resource owns the rule and listens to notes events:
The notes endpoints stay thin — they insert a note and return. The “keep the count fresh” rule lives with the resource that owns the count. Delete the feature later? Remove the handler file and re-run codegen. Nothing else changes.
Soft deletes flow through notes.update, since deleting a note sets deletedAt. Filtering deletedAt: null in the count keeps a deleted note out of the total. Wire a notes.delete handler too only if you hard-delete.

Decoupling history and audit

Want to keep a record of every change to a resource without threading audit logic through its endpoints? Let the audit resource subscribe. The users resource has no idea it’s being audited:
prevDocs gives you the before-and-after for free — no diff library, no change payload to assemble. This is the inverse of importing a historyService into the users endpoints: the dependency points into the audit resource, not out of users.

Integrating external systems

Events are the natural place to fan out to anything outside your database — analytics, sockets, email, webhooks. Ship ships two of these in the base template.

Analytics

users/handlers/sync-analytics.ts subscribes to users.insert and tracks a “New user created” event through analyticsService.

Realtime sockets

users/handlers/to-sockets.ts subscribes to users.update and pushes the updated row to that user’s browser over Socket.IO via ioEmitter.publishToUser.
The same shape covers webhooks — subscribe to a resource’s events and POST the payload out, turning your database mutations into real-time triggers for downstream systems:

When to use an event vs. a method

Not every consequence belongs in a handler. A quick rule:
  • Use an event when the side effect belongs to a different resource, is best-effort, or fans out to an external system — analytics, sockets, audit logs, denormalised counts, webhooks.
  • Use a method or do it inline when the work is part of the same transaction or is required for the write to be correct (e.g. validating a foreign key). Events fire after the write; they’re reactions, not preconditions.
Reading a resource’s handlers/ folders tells you everything that happens as a consequence of its writes — one place, no hidden coupling. That’s the payoff: every meaningful change flows through the same typed channel, and the structure is the documentation.

Next steps