# Server Hooks The `ServerHooks` trait provides a plugin system for extending the quicprochat server. Hooks fire at key points in the request lifecycle — message delivery, authentication, channel creation, and message fetch — allowing you to inspect, log, rate-limit, or reject operations without modifying server internals. --- ## Overview ```text Client RPC request └─ Validation (auth, rate limits, wire format) └─ Hook fires (on_message_enqueue, on_auth, etc.) ├─ HookAction::Continue → proceed to storage/delivery └─ HookAction::Reject("reason") → error returned to client ``` Hooks are called **synchronously** in the RPC handler path after validation but before storage. Keep hook implementations fast — offload heavy work (HTTP calls, disk I/O, analytics) to background tasks. --- ## The `ServerHooks` trait ```rust,ignore pub trait ServerHooks: Send + Sync { /// Called before a message is stored in the delivery queue. /// Return HookAction::Reject to prevent delivery. fn on_message_enqueue(&self, event: &MessageEvent) -> HookAction { HookAction::Continue } /// Called after a batch of messages is enqueued. fn on_batch_enqueue(&self, events: &[MessageEvent]) {} /// Called after a successful or failed login attempt. fn on_auth(&self, event: &AuthEvent) {} /// Called after a channel is created or looked up. fn on_channel_created(&self, event: &ChannelEvent) {} /// Called after messages are fetched from the delivery queue. fn on_fetch(&self, event: &FetchEvent) {} /// Called when a user completes OPAQUE registration. fn on_user_registered(&self, username: &str, identity_key: &[u8]) {} } ``` All methods have default no-op implementations. Override only the events you care about. --- ## Hook action ```rust,ignore pub enum HookAction { /// Allow the operation to proceed. Continue, /// Reject the operation with a reason (returned to the client as an error). Reject(String), } ``` Currently only `on_message_enqueue` can reject operations. Other hooks are observational (fire-and-forget). --- ## Event types ### `MessageEvent` Fired on `enqueue` and `batch_enqueue` RPC calls. | Field | Type | Description | |--------------------|-------------------|-------------| | `sender_identity` | `Option>` | Sender's 32-byte identity key (None in sealed sender mode). | | `recipient_key` | `Vec` | Recipient's 32-byte identity key. | | `channel_id` | `Vec` | 16-byte channel ID. | | `payload_len` | `usize` | Length of the encrypted payload in bytes. | | `seq` | `u64` | Server-assigned sequence number. | ### `AuthEvent` Fired after OPAQUE login completes (success or failure). | Field | Type | Description | |------------------|----------|-------------| | `username` | `String` | The username that attempted to authenticate. | | `success` | `bool` | Whether authentication succeeded. | | `failure_reason` | `String` | Failure reason (empty on success). | ### `ChannelEvent` Fired after a `createChannel` RPC call. | Field | Type | Description | |-----------------|------------|-------------| | `channel_id` | `Vec` | 16-byte channel ID. | | `initiator_key` | `Vec` | Identity key of the channel initiator. | | `peer_key` | `Vec` | Identity key of the peer. | | `was_new` | `bool` | True if this is a newly created channel. | ### `FetchEvent` Fired after a `fetch` or `fetchWait` RPC call. | Field | Type | Description | |-----------------|------------|-------------| | `recipient_key` | `Vec` | Identity key of the fetcher. | | `channel_id` | `Vec` | Channel ID being fetched from. | | `message_count` | `usize` | Number of messages returned. | --- ## Built-in implementations ### `NoopHooks` Does nothing. This is the default when no hooks are configured. ```rust,ignore pub struct NoopHooks; impl ServerHooks for NoopHooks {} ``` ### `TracingHooks` Logs all events via the `tracing` crate at info/debug level. ```rust,ignore pub struct TracingHooks; impl ServerHooks for TracingHooks { fn on_message_enqueue(&self, event: &MessageEvent) -> HookAction { tracing::info!( recipient_prefix = %hex_prefix(&event.recipient_key), payload_len = event.payload_len, seq = event.seq, "hook: message enqueued" ); HookAction::Continue } fn on_auth(&self, event: &AuthEvent) { if event.success { tracing::info!(username = %event.username, "hook: login success"); } else { tracing::warn!( username = %event.username, reason = %event.failure_reason, "hook: login failure" ); } } // ... other methods log similarly } ``` --- ## Writing a custom hook ### Example: payload size limiter ```rust,ignore use quicprochat_server::hooks::{ServerHooks, HookAction, MessageEvent}; struct PayloadLimiter { max_bytes: usize, } impl ServerHooks for PayloadLimiter { fn on_message_enqueue(&self, event: &MessageEvent) -> HookAction { if event.payload_len > self.max_bytes { return HookAction::Reject(format!( "payload too large: {} > {} bytes", event.payload_len, self.max_bytes )); } HookAction::Continue } } ``` ### Example: login auditor ```rust,ignore use quicprochat_server::hooks::{ServerHooks, AuthEvent}; struct LoginAuditor; impl ServerHooks for LoginAuditor { fn on_auth(&self, event: &AuthEvent) { if !event.success { eprintln!( "AUDIT: failed login for '{}': {}", event.username, event.failure_reason ); } } } ``` ### Example: composing multiple hooks ```rust,ignore use quicprochat_server::hooks::*; struct CompositeHooks { hooks: Vec>, } impl ServerHooks for CompositeHooks { fn on_message_enqueue(&self, event: &MessageEvent) -> HookAction { for hook in &self.hooks { if let HookAction::Reject(reason) = hook.on_message_enqueue(event) { return HookAction::Reject(reason); } } HookAction::Continue } fn on_auth(&self, event: &AuthEvent) { for hook in &self.hooks { hook.on_auth(event); } } // ... delegate other methods similarly } ``` --- ## Important considerations - **E2E encryption**: Message payloads are encrypted end-to-end. Hooks cannot inspect plaintext content — they see only metadata (sender, recipient, payload size, sequence number). - **Performance**: Hooks run synchronously in the RPC handler. A slow hook blocks the RPC response. Use `tokio::spawn` for async work. - **Thread safety**: `ServerHooks` requires `Send + Sync`. Use `Arc>` or lock-free structures for shared mutable state. - **Reject semantics**: Only `on_message_enqueue` supports rejection. Other hooks are informational — the operation proceeds regardless of what the hook does. --- ## Further reading - [Delivery Service Internals](delivery-service.md) -- how messages flow through the server - [Authentication Service Internals](authentication-service.md) -- OPAQUE auth flow - [Bot SDK](../getting-started/bot-sdk.md) -- build bots that interact with the server