A pluggable channel framework for notification services that wraps server-to-client messaging protocols. This package handles all cross-cutting concerns such as address verification and user preferences.
MGK (Messaging Gate Keeper) is a Rust-based framework that integrates with event streams to manage notification routing. It provides:
- Pluggable sender implementations - Easily swap different notification backends (email, SMS, push notifications, etc.)
- Preference management - Store and retrieve user notification preferences with validation
- Event stream integration - Subscribe to and process notification events
- Address verification - OTP-based address verification for secure notification delivery
- Batch preference updates - Set multiple preferences in a single operation
- Caching layer - Built-in caching for frequently accessed preferences
- Event publishing - Publishes confirmation events when preferences are verified
The main entry point for the framework. It coordinates:
- Event stream subscriptions for incoming notification events
- Sender implementations for delivering OTPs and notifications
- Preference management for storing user notification preferences
- HTTP routing configuration with authentication
A pluggable interface for notification delivery:
#[async_trait]
pub trait Sender: Send + Sync {
async fn send(
&self,
address: String,
subject: String,
message: String,
) -> Result<(), anyhow::Error>;
fn get_name(&self) -> String;
}The get_name() method returns a safe identifier for use as a SQL table-name suffix (lowercase letters, digits, and underscores only).
Manages user notification preferences with:
- Dynamic table creation - Creates sender-specific preference tables using the sender name
- In-memory caching - Moka-based LRU cache for performance
- OTP verification - One-time password validation for address confirmation
- Batch operations - Groups multiple preferences for atomic confirmation
- Event publishing - Publishes
ChannelConfirmedevents on successful verification
pub struct Preference {
pub subject: String, // Notification topic (max 64 chars)
pub address: String, // Delivery address (max 64 chars)
}pub struct PreferenceBatch {
pub preferences: Vec<Preference>, // At least one preference required
}All preferences in a batch must share the same address and will be confirmed together with a single OTP.
pub struct Token {
pub token: u32, // OTP token (6-digit number, range: 100000-999999)
}- Subscribes to event streams with subject pattern matching
- Processes events containing user metadata
- Routes notifications based on user preferences
- Only wakes up for configured subjects (not catch-all subscriptions)
- Graceful error handling and logging
- Set preferences: Generate OTP for address verification (accepts a batch)
- Confirm preferences: Store verified preferences after OTP validation
- Get preferences: Retrieve user's notification address for a subject
- Validation: Automatic field validation using the
validatorcrate - Batch atomic operations: All preferences in a batch are confirmed together
- Preference cache: 1000-entry LRU cache for user preferences (key:
(user, subject)) - Pending cache: 100-entry cache for OTP verification tokens with 5-minute TTL
- Reduces database queries for frequently accessed data
- Cache-aside pattern for preference retrieval
- Publishes
ChannelConfirmedevents after successful preference verification - Event subject:
contact.channel.confirmed - Includes user, subject, and confirmed address information
- actix-web - HTTP framework
- async-trait - Async trait support
- sqlx - Database access with SQLite
- serde/serde_json - JSON serialization
- moka - Async caching
- tracing - Structured logging
- validator - Input validation
- rand - Random number generation for OTP
- actixutils - Utilities for Actix authentication
- typed-eventbus - Event streaming interface
use mgk::Module;
use sqlx::SqlitePool;
use std::sync::Arc;
// Create a database pool
let pool = SqlitePool::connect("sqlite://notifications.db").await?;
// Create your event stream
let typed_eventbus = Arc::new(/* your EventStream implementation */);
// Create your custom sender
let sender = Arc::new(MyCustomSender);
// Define which subjects this module will handle
let subjects = vec!["order.created".to_string(), "payment.completed".to_string()];
// Create the module
let module = Module::new(pool, typed_eventbus, sender, subjects).await?;Configure with an Actix-web app:
use actix_web::web;
let mut config = web::ServiceConfig::default();
module.config(&mut config, "/notifications");
// Routes will be available at /notifications/preferences/*All endpoints require authentication (via Auth<Identity>).
The module provides the following HTTP routes (prefix: /notifications):
-
POST /preferences/set- Set a batch of preferences and receive OTP- Body:
PreferenceBatchwith one or more preferences sharing the same address - Response:
{ "nonce": "..." }- use this nonce in the confirm request
- Body:
-
POST /preferences/confirm- Confirm preferences with OTP token- Body:
{ "nonce": "...", "token": 123456 } - Response: 200 OK if successful
- Body:
-
GET /preferences/get- Retrieve user preference for a subject- Query:
?subject=order.created - Response: The notification address or 404 if not found
- Query:
-
Client requests to set preferences:
POST /notifications/preferences/set { "preferences": [ { "subject": "order.created", "address": "user@example.com" }, { "subject": "payment.completed", "address": "user@example.com" } ] } -
Server responds with nonce and sends OTP out-of-band:
{ "nonce": "a1b2c3d4e5f6g7h8" }(OTP sent to
user@example.comvia the configured sender) -
Client confirms with OTP and nonce:
POST /notifications/preferences/confirm { "nonce": "a1b2c3d4e5f6g7h8", "token": 123456 } -
Preferences are now stored and confirmed events are published
Implement the Sender trait to integrate with your notification service:
use async_trait::async_trait;
use mgk::Sender;
struct EmailSender {
api_key: String,
}
#[async_trait]
impl Sender for EmailSender {
async fn send(
&self,
address: String,
subject: String,
message: String,
) -> Result<(), anyhow::Error> {
// Send notification via your service
println!("Email to {}: {} - {}", address, subject, message);
Ok(())
}
fn get_name(&self) -> String {
"email".to_string()
}
}The sender name (get_name()) is used to create a table named {sender_name}_preferences in the database.
The framework dynamically creates SQLite tables based on the sender name. For a sender with name "email", the table structure is:
CREATE TABLE IF NOT EXISTS email_preferences (
user TEXT NOT NULL,
subject TEXT NOT NULL,
address TEXT NOT NULL,
UNIQUE(user, subject)
);Multiple senders can store preferences in the same database (each in their own table).
Incoming events are expected to contain metadata in the following format:
{
"metadata": {
"user_id": "user_123"
},
"data": {
/* your event data */
}
}The framework extracts the user_id from event metadata and uses it to look up notification preferences for matching subjects.
When a preference is confirmed, an event is published:
{
"metadata": {
"source": "mgk"
},
"data": {
"user": "user_123",
"subject": "order.created",
"address": "user@example.com"
}
}Subject: contact.channel.confirmed
The framework implements comprehensive error handling:
- Invalid OTP tokens (mismatched or expired)
- Invalid batch data (empty preferences, mismatched addresses)
- Missing user preferences
- Database errors
- JSON parsing errors
- Missing user ID in event metadata
- Invalid sender names (non-alphanumeric)
- Unauthorized requests (missing authentication)
All errors are logged with context for debugging. Failed OTP deliveries are logged but do not prevent the pending entry from being recorded.
- Caching: Preferences are cached in-memory (1000 entries LRU) to reduce database hits
- Async/await: All operations are async for non-blocking I/O
- Connection pooling: SQLx manages a pool of database connections
- OTP caching: Temporary storage of pending verifications (100 entries with 5-minute TTL)
- Selective subscriptions: Module subscribes only to configured subjects, not catch-all events
- Subject filtering: Only processes events for subjects with stored preferences
mgk/
├── src/
│ ├── lib.rs # Main module, Module struct, and Sender trait
│ └── prefs/
│ ├── mod.rs # Module exports
│ ├── db.rs # Preferences, database, and cache logic
│ ├── handlers.rs # Request handlers for HTTP endpoints
│ └── routes.rs # Route configuration
├── Cargo.toml # Package manifest
└── README.md # This file
- Rust 1.70+
- SQLite 3.x
cargo buildcargo testcargo runThis is an open-source project. Contributions are welcome! Please fork the repository and submit pull requests.
Not currently licensed. See repository for details.
Repository: https://github.com/Ferrumec/mgk
Language: Rust
Current Version: 0.1.0
Edition: 2024