Lifecycle Hooks
Inject custom logic at specific points in the data lifecycle — auditing, enrichment, validation, and cleanup — without cluttering your business logic.
Overview
Section titled “Overview”Hooks let you observe and shape writes as they flow through the repository. Register them with
on(event, fn); the callback may be synchronous or async (the repository awaits it). A single
event can carry multiple listeners, and they run in registration order.
userRepo.on('afterCreate', async user => { await auditLog.record('user_created', user);});Hook execution order
Section titled “Hook execution order”before*hooks run first and can enrich or normalize the payload before schema validation.- The validated payload is the one persisted to Firestore.
after*hooks run only after a successful write.- Bulk operations fire the corresponding
beforeBulk*/afterBulk*events with the same ordering guarantees.
Because before* runs before validation, it is the correct place to fill in defaults, coerce
values, or reject a write early. See CRUD operations for the write methods
these hooks wrap.
Available hooks
Section titled “Available hooks”- Single operations:
beforeCreate,afterCreate,beforeUpdate,afterUpdate,beforeDelete,afterDelete - Bulk operations:
beforeBulkCreate,afterBulkCreate,beforeBulkUpdate,afterBulkUpdate,beforeBulkDelete,afterBulkDelete
Hook payloads
Section titled “Hook payloads”| Event | Payload |
|---|---|
beforeCreate |
The create payload (before validation) |
afterCreate |
The created document, including the generated id |
beforeUpdate |
The update payload plus the target id (data & { id }) |
afterUpdate |
{ id } |
beforeDelete / afterDelete |
The full persisted document ({ ...data, id }) |
beforeBulkCreate / afterBulkCreate |
An array of created documents (each including id) |
beforeBulkUpdate |
{ id, data }[] |
afterBulkUpdate |
{ ids: string[] } |
beforeBulkDelete / afterBulkDelete |
{ ids: string[]; documents: (T & { id })[] } |
Delete hooks (single and bulk) receive the full persisted document(s) as they existed before
deletion, so cleanup logic has access to every field, not just the id.
Examples
Section titled “Examples”// Log all user creationsuserRepo.on('afterCreate', async user => { console.log(`User created: ${user.id}`); await auditLog.record('user_created', user);});
// Send welcome emailuserRepo.on('afterCreate', async user => { await sendWelcomeEmail(user.email);});
// Validate business rules before updateorderRepo.on('beforeUpdate', data => { if (data.status === 'shipped' && !data.trackingNumber) { throw new Error('Tracking number required for shipped orders'); }});
// Enrich create payload before validation (e.g., timestamps/defaults)orderRepo.on('beforeCreate', data => { data.createdAt = new Date().toISOString(); data.updatedAt = new Date().toISOString();});
// Clean up related data after deletionuserRepo.on('afterDelete', async user => { await orderRepo.query().where('userId', '==', user.id).delete();});In the last example, query().delete() is a query-level bulk write that does not fire delete
hooks (see below) — which is exactly what you want here, since it avoids re-triggering cleanup logic
recursively.
When hooks do not run
Section titled “When hooks do not run”Hooks are wired into the per-document and bulk methods on the repository. Two paths differ from that standard flow:
- Query-level writes.
query().update(data)andquery().delete()operate directly on the matched documents and do not run any hooks (including thebeforeBulk*/afterBulk*events). If you need hook behavior, read the ids and route throughbulkUpdate/bulkDeleteinstead. See Queries. - Transactions —
before*only. InsiderunInTransaction((tx, repo) => { ... }), the transaction-scopedrepo’s write helpers (createInTransaction,updateInTransaction,patchInTransaction,deleteInTransaction) do run theirbefore*hooks (before validation and the staged write). Theirafter*hooks do not run — the transaction has not committed while the callback executes, so post-commit side effects belong afterrunInTransactionresolves. See Transactions.