Lawhive Framework
FrameworkEvents

Event System

Type-safe event definitions with effects, transforms, and subscriptions

The event system provides a type-safe way to define domain events that trigger side effects and can be persisted, replayed, and subscribed to.

Core Concepts

Event Types

There are two types of events:

  • Aggregate Events - Events tied to an entity (e.g., task.created, user.updated). These have an aggregate ID and type for history queries.
  • Ephemeral Events - Fire-and-forget events not tied to an entity. Useful for notifications or cross-cutting concerns.

Event Flow

Defining Events

Use the ev() factory to create events:

import { ev } from "@lawhive/framework"
import { z } from "zod"

const { event, ephemeral, router } = ev()

// Aggregate event with effect
const taskCreated = event({
  schema: z.object({ name: z.string() }),
  effect: async (input) => {
    return await prisma.task.create({ data: input })
  },
  selectId: ({ output }) => output.id,
})

// With transform (payload differs from input)
const taskRenamed = event({
  schema: z.object({ id: z.string(), name: z.string() }),
  effect: async (input) => {
    return await prisma.task.update({
      where: { id: input.id },
      data: { name: input.name },
    })
  },
  transform: (input, output) => ({
    id: output.id,
    oldName: output.name, // from before update
    newName: input.name,
  }),
  selectId: ({ payload }) => payload.id,
})

// Ephemeral event (no effect, no aggregate)
const notificationSent = ephemeral({
  schema: z.object({ userId: z.string(), message: z.string() }),
})

Event Routers

Group related events into routers:

const taskEvents = router({
  created: taskCreated,
  renamed: taskRenamed,
  deleted: taskDeleted,
})

const userEvents = router({
  registered: userRegistered,
  updated: userUpdated,
})

// Combine into root router
const events = router({
  tasks: taskEvents,
  users: userEvents,
})

Creating an Event Client

The event client provides the publishing API:

import { createEventClient } from "@lawhive/framework"
import { createEventStoreAdapter } from "@lawhive/framework/adapters"

const eventClient = createEventClient(events, {
  adapters: [
    createEventStoreAdapter({ storage: myEventStorage }),
  ],
  createContext: async (fn) => {
    return prisma.$transaction(async (tx) => {
      return fn({ transaction: tx })
    })
  },
})

Publishing Events

// Single event - returns the effect result
const task = await eventClient.tasks.created.publish({ name: "My Task" })

// Build event DTOs without publishing (for batch)
const dto1 = eventClient.tasks.created.build({ id: "1", name: "Task 1" })
const dto2 = eventClient.tasks.created.build({ id: "2", name: "Task 2" })

// Publish multiple events atomically
await eventClient.publishMany([dto1, dto2], { atomic: true })

Subscriptions

Add subscribers to react to events:

const taskCreated = event({
  schema: z.object({ name: z.string() }),
  effect: async (input) => prisma.task.create({ data: input }),
  selectId: ({ output }) => output.id,
  subscribers: [
    async (payload) => {
      await sendNotification(`Task ${payload.name} created`)
    },
    async (payload) => {
      await updateSearchIndex("tasks", payload)
    },
  ],
})

Event Handler

For consuming events (e.g., from a queue):

import { createEventHandler } from "@lawhive/framework"

const handler = createEventHandler(events)

// Process incoming event
const result = await handler({
  id: "evt_123",
  type: "aggregate",
  key: "tasks.created",
  aggregateId: "task_456",
  aggregateType: "tasks",
  payload: { id: "task_456", name: "My Task" },
})

Next Steps