Guide

Lifecycle hooks

Run ordered domain behavior around transactional mutations.

Hooks are registered per logical table name and mutation phase.

Register hooks

const hooks = createHooksRegistry()

hooks.onBeforeCreate('users', normalizeEmail)
hooks.onAfterCreate('users', createProfile)

const engine = createQueryEngine(registry, adapter, hooks)

Before hooks

Before-create and before-update hooks transform data. The result of one hook becomes the input of the next.

hooks.onBeforeCreate('users', async (data, context) => ({
  ...data,
  email: String(data.email).trim().toLowerCase(),
  createdBy: (context.user as { id?: string } | undefined)?.id
}))

Before-delete hooks receive the effective QueryFilter and return no transformed value.

hooks.onBeforeDelete('users', async (where, context) => {
  if (!canDeleteUsers(context.user)) {
    throw new Error('Deletion is not allowed.')
  }
})

After hooks

After-create and after-update hooks receive the entity returned by the adapter.

hooks.onAfterUpdate('users', async (entity, context) => {
  await context.adapter.insert('auditEntries', {
    id: crypto.randomUUID(),
    entityId: entity.id,
    operation: context.operation
  })
})

After-delete hooks receive context only.

Hook context

interface HookContext {
  table: string
  operation: 'create' | 'update' | 'delete'
  metadata: TableMetadata
  adapter: DatabaseAdapter
  requestId?: string
  tenantId?: string
  user?: unknown
  [key: string]: unknown
}

The adapter is bound to the active transaction, so related database work performed by a hook remains atomic.

Per-operation behavior

OperationBefore inputAfter input
createresolved creation datainserted entity
updateOnepatchupdated entity
updateManyone shared patchhook runs for each updated entity
deleteOneprimary-key filter of loaded entitycontext
deleteManyoriginal filtercontext

Failure semantics

Hooks run sequentially. If a before or after hook throws, the adapter transaction callback rejects and Drizzle rolls the transaction back.

Do not perform slow, non-transactional external calls directly inside hooks. Emit a Domain Event from an application transaction and let the built-in outbox processor deliver emails, webhooks or messages asynchronously.

Good uses

  • normalization and derived fields;
  • authorization checks that require mutation context;
  • tenant field injection;
  • maintaining domain records that belong to the same transaction;
  • maintaining related records atomically;
  • rejecting invalid domain transitions.

Hooks complement validators. Use validators for data validity and hooks for mutation behavior.

Use the dedicated Audit Log and Domain Events APIs instead of hand-written audit or outbox hooks.