Adapters
Composable adapters for event storage, logging, and delivery
Adapters are composable handlers that process events during publishing. All adapters in a client participate in the same context, ensuring atomicity across operations.
How Adapters Work
When you publish an event, all configured adapters receive the event batch within the same context (typically a database transaction):
Built-in Adapters
Event Store Adapter
Persists events to a database table for audit and replay.
Prisma Schema
The event store requires an Event table. Add this to your Prisma schema:
model Event {
id String @id @default(uuid())
timestamp DateTime
type EventType
key String
aggregateId String?
aggregateType String?
payload Json
@@index([aggregateType, aggregateId])
@@index([key])
}
enum EventType {
aggregate
ephemeral
}The aggregateType and aggregateId fields link aggregate events to their entity, enabling the History API. Indexing these fields is important for efficient history queries.
Usage
import { createEventStoreAdapter } from "@lawhive/framework/adapters"
const eventStoreAdapter = createEventStoreAdapter({
storage: {
createMany: async (records, ctx) => {
const results = await ctx.transaction.event.createManyAndReturn({
data: records,
})
return { ids: results.map((r) => r.id) }
},
},
})Storage Interface
type EventStorage<TContext> = {
createMany: (
records: EventRecord[],
ctx: TContext,
) => Promise<{ ids: string[] }>
}
type EventRecord = {
id?: string
timestamp: Date
type: "aggregate" | "ephemeral"
key: string
aggregateId?: string
aggregateType?: string
payload: unknown
}Logger Adapter
Logs events during development and debugging:
import { createLoggerAdapter } from "@lawhive/framework/adapters"
const loggerAdapter = createLoggerAdapter({
level: "debug",
includePayload: true,
prefix: "[events]",
})Options:
| Option | Type | Default | Description |
|---|---|---|---|
level | "debug" | "info" | "warn" | "error" | "info" | Log level |
includePayload | boolean | false | Include event payload in logs |
prefix | string | "[event]" | Prefix for log messages |
log | function | console[level] | Custom log function |
Outbox Adapter
Implements the transactional outbox pattern for reliable delivery. See Outbox Pattern for details.
Composing Adapters
Chain multiple adapters together:
import { composeAdapters } from "@lawhive/framework/adapters"
const productionAdapter = composeAdapters([
createLoggerAdapter({ level: "info" }),
createEventStoreAdapter({ storage: eventStorage }),
createOutboxAdapter({ storage: outboxStorage }),
])
const client = createEventClient(router, {
adapters: [productionAdapter],
createContext: async (fn) => prisma.$transaction((tx) => fn({ tx })),
})Conditional Adapters
Filter which events go to specific adapters:
import { createConditionalAdapter } from "@lawhive/framework/adapters"
// Only aggregate events go to the outbox
const aggregateOnlyOutbox = createConditionalAdapter(
createOutboxAdapter({ storage: outboxStorage }),
(event) => event.type === "aggregate",
)Creating Custom Adapters
Adapters implement a simple interface:
import type { EventAdapter } from "@lawhive/framework"
type MyContext = {
transaction: PrismaTransactionClient
userId: string
}
const myAdapter: EventAdapter<MyContext> = {
name: "my-adapter",
onPublish: async ({ events, ctx }) => {
for (const event of events) {
await ctx.transaction.customTable.create({
data: {
eventKey: event.key,
userId: ctx.userId,
payload: event.payload,
},
})
}
},
}Context Pattern
The createContext callback wraps all adapter calls:
const client = createEventClient(router, {
adapters: [eventStoreAdapter, outboxAdapter],
createContext: async (fn) => {
// Everything runs in this transaction
return prisma.$transaction(async (tx) => {
const ctx = {
transaction: tx,
logger: createLogger(),
requestId: getRequestId(),
}
return fn(ctx)
})
},
})This ensures:
- All adapters see the same context
- Database operations are atomic
- Rollback on any failure
Noop Adapter
For testing or conditional inclusion:
import { createNoopAdapter } from "@lawhive/framework/adapters"
const adapters = [
createEventStoreAdapter({ storage }),
isProduction ? createOutboxAdapter({ storage }) : createNoopAdapter(),
]