Complete guide to HushNet Backend code organization.
HushNet-Backend/
├── src/ # Source code
│ ├── main.rs # Application entry point
│ ├── app_state.rs # Shared application state
│ │
│ ├── controllers/ # HTTP request handlers
│ │ ├── mod.rs
│ │ ├── chats_controller.rs
│ │ ├── device_controller.rs
│ │ ├── keys_controller.rs
│ │ ├── messages_controller.rs
│ │ ├── root_controller.rs
│ │ ├── session_controller.rs
│ │ └── user_controller.rs
│ │
│ ├── routes/ # Route definitions
│ │ ├── mod.rs
│ │ ├── chats.rs
│ │ ├── devices.rs
│ │ ├── messages.rs
│ │ ├── root.rs
│ │ ├── sessions.rs
│ │ ├── users.rs
│ │ └── websocket.rs
│ │
│ ├── repository/ # Data access layer
│ │ ├── mod.rs
│ │ ├── chat_repository.rs
│ │ ├── device_repository.rs
│ │ ├── enrollment_token_repository.rs
│ │ ├── keys_repository.rs
│ │ ├── message_repository.rs
│ │ ├── session_repository.rs
│ │ └── user_repository.rs
│ │
│ ├── services/ # Business logic
│ │ ├── mod.rs
│ │ └── auth.rs
│ │
│ ├── models/ # Data structures
│ │ ├── mod.rs
│ │ ├── chat.rs
│ │ ├── device.rs
│ │ ├── enrollment_token.rs
│ │ ├── keys.rs
│ │ ├── message.rs
│ │ ├── realtime.rs
│ │ ├── session.rs
│ │ └── user.rs
│ │
│ ├── middlewares/ # HTTP middlewares
│ │ ├── mod.rs
│ │ └── auth.rs
│ │
│ ├── realtime/ # Real-time communication
│ │ ├── mod.rs
│ │ ├── listener.rs # PostgreSQL LISTEN
│ │ └── websocket.rs # WebSocket handlers
│ │
│ ├── utils/ # Utility functions
│ │ ├── mod.rs
│ │ └── crypto_utils.rs
│ │
│ └── db/ # Database utilities
│ ├── mod.rs
│ ├── connection.rs
│ └── models.rs
│
├── sql_models/ # Database schemas
│ └── seed.sql
│
├── docs/ # Documentation
│ ├── API.md
│ ├── DATABASE.md
│ ├── SECURITY.md
│ ├── REALTIME.md
│ ├── STRUCTURE.md
│ ├── INSTALLATION.md
│ ├── CONFIGURATION.md
│ ├── DOCKER.md
│ ├── DEVELOPMENT.md
│ ├── CONTRIBUTING.md
│ ├── ROADMAP.md
│ └── CHANGELOG.md
│
├── target/ # Build artifacts (ignored)
├── Cargo.toml # Rust dependencies
├── Cargo.lock # Dependency lock file
├── Dockerfile # PostgreSQL Docker image
├── .env # Environment variables (not committed)
├── .gitignore # Git ignore rules
├── README.md # Main documentation
└── LICENSE # MIT License
┌─────────────────────────────────────────────┐
│ HTTP/WebSocket │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Routes Layer │
│ (URL mapping, parameter extraction) │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Middleware Layer │
│ (Authentication, logging, CORS) │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Controllers Layer │
│ (Request handling, response formatting) │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Services Layer │
│ (Business logic, validation) │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Repository Layer │
│ (Database operations, queries) │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Database (PostgreSQL) │
└─────────────────────────────────────────────┘
Entry point of the application.
Responsibilities:
- Initialize logging
- Load environment variables
- Connect to PostgreSQL
- Start PostgreSQL listeners
- Setup routes and middlewares
- Start HTTP server
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
// 1. Initialize tracing
tracing_subscriber::fmt::init();
// 2. Load .env
dotenvy::dotenv().ok();
// 3. Connect to database
let pool = PgPool::connect(&database_url).await?;
// 4. Setup real-time
let (tx, _rx) = broadcast::channel::<RealtimeEvent>(100);
tokio::spawn(start_pg_listeners(pool.clone(), tx.clone()));
// 5. Build application
let app = Router::new()
.merge(routes::users::routes())
.merge(routes::devices::routes())
// ... more routes
.layer(Extension(tx));
// 6. Start server
axum::serve(listener, app).await.unwrap();
Ok(())
}Shared application state passed to all handlers.
#[derive(Clone)]
pub struct AppState {
pub pool: PgPool,
pub jwt_secret: String, // Note: Not currently used
}Defines URL routes and maps them to controllers.
Example (routes/users.rs):
pub fn routes() -> Router<AppState> {
Router::new()
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user_by_id))
.route("/login", post(login_user))
}Handles HTTP requests and returns responses.
Responsibilities:
- Extract request parameters
- Call services/repositories
- Format responses
- Handle errors
Example (controllers/user_controller.rs):
pub async fn create_user(
State(state): State<AppState>,
Json(payload): Json<CreateUserRequest>,
) -> Result<Json<User>, StatusCode> {
let user = user_repository::create_user(&state.pool, &payload.username)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(user))
}Request/response interceptors.
Authentication Middleware (middlewares/auth.rs):
pub struct AuthenticatedDevice(pub Devices);
#[async_trait]
impl FromRequestParts<AppState> for AuthenticatedDevice {
type Rejection = (StatusCode, String);
async fn from_request_parts(
parts: &mut Parts,
state: &AppState,
) -> Result<Self, Self::Rejection> {
// 1. Extract headers (X-Identity-Key, X-Signature, X-Timestamp)
// 2. Verify timestamp (anti-replay)
// 3. Verify Ed25519 signature
// 4. Fetch device from database
// 5. Return authenticated device
}
}Usage in controllers:
pub async fn protected_endpoint(
AuthenticatedDevice(device): AuthenticatedDevice,
State(state): State<AppState>,
) -> Response {
// `device` is guaranteed to be authenticated
}Database access layer using SQLx.
Responsibilities:
- Execute SQL queries
- Map database rows to Rust structs
- Handle database errors
Example (repository/user_repository.rs):
pub async fn get_user_by_id(
pool: &PgPool,
user_id: &Uuid,
) -> Result<User, sqlx::Error> {
sqlx::query_as::<_, User>(
"SELECT id, username, created_at FROM users WHERE id = $1"
)
.bind(user_id)
.fetch_one(pool)
.await
}Business logic and complex operations.
Example (services/auth.rs):
pub fn verify_signature(
public_key: &[u8; 32],
message: &[u8],
signature: &[u8; 64],
) -> Result<(), SignatureError> {
let verifying_key = VerifyingKey::from_bytes(public_key)?;
let sig = Signature::from_bytes(signature);
verifying_key.verify(message, &sig)
}Data structures and serialization.
Example (models/user.rs):
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
pub struct User {
pub id: Uuid,
pub username: String,
pub created_at: chrono::NaiveDateTime,
}
#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
pub username: String,
}Real-time communication implementation.
listener.rs: PostgreSQL LISTEN
pub async fn start_pg_listeners(
pool: PgPool,
tx: broadcast::Sender<RealtimeEvent>,
) {
tokio::spawn(async move {
let mut listener = PgListener::connect_with(&pool).await?;
listener.listen_all(vec![
"messages_channel",
"sessions_channel",
"devices_channel",
]).await?;
loop {
while let Ok(Some(notif)) = listener.try_recv().await {
let event: RealtimeEvent = serde_json::from_str(notif.payload())?;
tx.send(event)?;
}
}
});
}websocket.rs: WebSocket handler
pub async fn websocket_handler(
ws: WebSocketUpgrade,
Query(params): Query<WsParams>,
Extension(tx): Extension<broadcast::Sender<RealtimeEvent>>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| handle_socket(socket, params.user_id, tx))
}
async fn handle_socket(
socket: WebSocket,
user_id: Uuid,
tx: broadcast::Sender<RealtimeEvent>,
) {
let mut rx = tx.subscribe();
let (mut sender, mut receiver) = socket.split();
tokio::spawn(async move {
while let Ok(event) = rx.recv().await {
if event.user_id == user_id {
let json = serde_json::to_string(&event)?;
sender.send(Message::Text(json)).await?;
}
}
});
}Helper functions and utilities.
Example (utils/crypto_utils.rs):
pub fn decode_base64(input: &str) -> Result<Vec<u8>, base64::DecodeError> {
base64::engine::general_purpose::STANDARD.decode(input)
}
pub fn encode_base64(input: &[u8]) -> String {
base64::engine::general_purpose::STANDARD.encode(input)
}1. Client → POST /messages
Headers: X-Identity-Key, X-Signature, X-Timestamp
Body: { chat_id, to_device_id, header, ciphertext }
2. routes/messages.rs
→ Maps to messages_controller::send_message
3. middlewares/auth.rs
→ Extracts AuthenticatedDevice
→ Verifies Ed25519 signature
4. controllers/messages_controller.rs
→ Extracts request body
→ Calls message_repository::create_message
5. repository/message_repository.rs
→ Executes INSERT query
→ Returns created message
6. PostgreSQL
→ messages_notify_trigger fires
→ NOTIFY messages_channel
7. realtime/listener.rs
→ Receives NOTIFY
→ Broadcasts to WebSocket handlers
8. realtime/websocket.rs
→ Filters by user_id
→ Sends to connected client
9. Client ← WebSocket message
{ type: "message", chat_id: "...", user_id: "..." }
main.rs
├─ routes/* (all route modules)
├─ app_state.rs
├─ realtime/listener.rs
└─ models/realtime.rs
routes/*
├─ controllers/*
└─ app_state.rs
controllers/*
├─ repository/*
├─ services/*
├─ models/*
├─ middlewares/auth.rs
└─ app_state.rs
middlewares/auth.rs
├─ models/device.rs
├─ repository/device_repository.rs
├─ utils/crypto_utils.rs
└─ app_state.rs
repository/*
├─ models/*
└─ sqlx
services/*
├─ models/*
└─ utils/*
realtime/listener.rs
├─ models/realtime.rs
└─ sqlx
realtime/websocket.rs
├─ models/realtime.rs
└─ axum/extract/ws
src/
├─ controllers/
│ ├─ user_controller.rs
│ └─ user_controller_test.rs # Unit tests
│
├─ repository/
│ ├─ user_repository.rs
│ └─ user_repository_test.rs # Integration tests with test DB
│
└─ services/
├─ auth.rs
└─ auth_test.rs # Unit tests
tests/
├─ integration/
│ ├─ api_tests.rs # Full API tests
│ └─ websocket_tests.rs # WebSocket tests
│
└─ common/
└─ mod.rs # Test utilities
- One concern per module: Each module has a single responsibility
- Clear boundaries: Layers don't skip levels (controller → repository OK, route → repository NOT OK)
- Minimal public API: Expose only what's necessary
- Internal modules: Use
modfor module-local utilities
- Files:
snake_case(e.g.,user_controller.rs) - Structs:
PascalCase(e.g.,AuthenticatedDevice) - Functions:
snake_case(e.g.,get_user_by_id) - Constants:
SCREAMING_SNAKE_CASE(e.g.,MAX_RETRIES)
// Use Result types
pub async fn get_user(id: Uuid) -> Result<User, Error> {
// ...
}
// Use anyhow for application errors
use anyhow::{Context, Result};
pub async fn complex_operation() -> Result<()> {
let user = get_user(id)
.await
.context("Failed to fetch user")?;
Ok(())
}