Files
quicproquo/docs/src/internals/server-hooks.md
Christian Nennemann 2e081ead8e chore: rename quicproquo → quicprochat in docs, Docker, CI, and packaging
Rename all project references from quicproquo/qpq to quicprochat/qpc
across documentation, Docker configuration, CI workflows, packaging
scripts, operational configs, and build tooling.

- Docker: crate paths, binary names, user/group, data dirs, env vars
- CI: workflow crate references, binary names, artifact names
- Docs: all markdown files under docs/, SDK READMEs, book.toml
- Packaging: OpenWrt Makefile, init script, UCI config (file renames)
- Scripts: justfile, dev-shell, screenshot, cross-compile, ai_team
- Operations: Prometheus config, alert rules, Grafana dashboard
- Config: .env.example (QPQ_* → QPC_*), CODEOWNERS paths
- Top-level: README, CONTRIBUTING, ROADMAP, CLAUDE.md
2026-03-21 19:14:06 +01:00

7.4 KiB

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

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

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

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<Vec<u8>> Sender's 32-byte identity key (None in sealed sender mode).
recipient_key Vec<u8> Recipient's 32-byte identity key.
channel_id Vec<u8> 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<u8> 16-byte channel ID.
initiator_key Vec<u8> Identity key of the channel initiator.
peer_key Vec<u8> 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<u8> Identity key of the fetcher.
channel_id Vec<u8> 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.

pub struct NoopHooks;
impl ServerHooks for NoopHooks {}

TracingHooks

Logs all events via the tracing crate at info/debug level.

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

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

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

use quicprochat_server::hooks::*;

struct CompositeHooks {
    hooks: Vec<Box<dyn ServerHooks>>,
}

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<Mutex<_>> 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