Feature Description
Define a small set of well-known lifecycle hooks at the critical paths of the platform: before ingestion accepts a payload, before a search query executes, before an alert rule is evaluated, and before an outbound webhook is dispatched. The default implementation is a no-op. Operators or downstream distributions can register handlers that observe, mutate, or reject the operation.
Problem/Use Case
Several adjacent concerns want to intercept these paths:
- Quota enforcement (reject ingestion when the org is over its monthly limit)
- Custom rate limiting policies beyond the global ones
- Compliance hooks (e.g. additional logging of every alert evaluation for regulated environments)
- Synchronous PII checks beyond the existing masking pipeline
Each of these can be solved ad-hoc inside a Fastify hook, but doing so means business logic gets scattered across routes and middleware, and BullMQ-driven paths (alerts, webhooks) don't benefit at all. A small, named set of hooks gives all these concerns a single home.
Proposed Solution
Define hooks as async functions registered against well-known phase names. Each hook receives a typed context and may throw a typed error to abort the operation.
hooks.register('beforeIngest', async (ctx) => {
// ctx: { organizationId, projectId, payloadSize, eventCount }
// throw new HookRejectionError('quota.exceeded', 'Monthly limit reached')
})
hooks.register('beforeQuery', async (ctx) => { /* ... */ })
hooks.register('beforeAlertEvaluation', async (ctx) => { /* ... */ })
hooks.register('beforeWebhookDispatch', async (ctx) => { /* ... */ })
Initial set:
| Hook |
When |
Context fields |
beforeIngest |
After auth, before reservoir write |
org, project, eventCount, byteSize |
beforeQuery |
After parsing, before reservoir read |
org, project, queryShape, timeRange |
beforeAlertEvaluation |
In BullMQ worker, before rule execution |
org, ruleId, ruleType |
beforeWebhookDispatch |
Before outbound HTTP call |
org, webhookId, targetHost |
In OSS, no hooks are registered by default. The performance overhead of an empty hook list is negligible.
Alternatives Considered
- Per-feature middleware. Works for HTTP paths but doesn't reach BullMQ workers. Inconsistent shape. Rejected.
- A general event bus. Too broad. We want a small, intentional set of named extension points, not "subscribe to anything that happens." Easier to reason about and to keep stable.
- Wait until a concrete need lands. The two concrete needs (quota, custom rate limits) are already on the horizon. Adding hooks reactively means changing the same code paths twice.
Implementation Details (Optional)
- Hooks are typed per phase.
beforeIngest and beforeQuery get different context shapes; the registry enforces this via discriminated unions.
- Hook execution is sequential within a phase, in registration order. Any thrown error short-circuits and propagates with a typed rejection reason.
- All hooks receive the current
RequestContext (see #request-context-propagation issue) — they don't need to thread it manually.
- A
HookRejectionError carries a machine-readable code and a human-readable message. The caller decides whether to surface the message to the user (typically yes for quota errors, no for internal policy errors).
- Document the hook contract clearly: what fields are stable, what can be mutated, and what guarantees exist about order of execution.
- No hook should perform unbounded I/O. Document this convention; we may add a soft timeout later.
Priority
Target Users
- Operators wanting to enforce custom organizational policies without forking the codebase
- Self-hosters needing fine-grained control over what can be ingested, queried, or evaluated
- Downstream distributions that need a stable, narrow extension surface
Contribution
Feature Description
Define a small set of well-known lifecycle hooks at the critical paths of the platform: before ingestion accepts a payload, before a search query executes, before an alert rule is evaluated, and before an outbound webhook is dispatched. The default implementation is a no-op. Operators or downstream distributions can register handlers that observe, mutate, or reject the operation.
Problem/Use Case
Several adjacent concerns want to intercept these paths:
Each of these can be solved ad-hoc inside a Fastify hook, but doing so means business logic gets scattered across routes and middleware, and BullMQ-driven paths (alerts, webhooks) don't benefit at all. A small, named set of hooks gives all these concerns a single home.
Proposed Solution
Define hooks as async functions registered against well-known phase names. Each hook receives a typed context and may throw a typed error to abort the operation.
Initial set:
beforeIngestbeforeQuerybeforeAlertEvaluationbeforeWebhookDispatchIn OSS, no hooks are registered by default. The performance overhead of an empty hook list is negligible.
Alternatives Considered
Implementation Details (Optional)
beforeIngestandbeforeQueryget different context shapes; the registry enforces this via discriminated unions.RequestContext(see #request-context-propagation issue) — they don't need to thread it manually.HookRejectionErrorcarries a machine-readable code and a human-readable message. The caller decides whether to surface the message to the user (typically yes for quota errors, no for internal policy errors).Priority
Target Users
Contribution