Backend Architecture (Rust / Tauri)
Layered architecture
graph TB
FE[Frontend<br/>invoke('command_name', args)] --> IPC[Tauri IPC<br/>snake_case wire format both sides]
IPC --> CMD[Commands<br/>src/core/src/commands/*.rs<br/>thin validation + delegation]
CMD --> REPO[Repositories<br/>src/core/src/repositories/*.rs<br/>SQL + entity structs + ownership checks]
REPO --> DBMOD[Database<br/>src/core/src/database/<br/>pool init, migrations, seeds]
DBMOD --> SQLITE[(imh.db)]
src/core/src/lib.rs wires everything together: it configures the tauri-plugin-dialog plugin, runs the database::init() setup hook (which opens the pool, runs migrations/seeds, and calls app.manage(pool)), and registers every command in a single tauri::generate_handler![...] call.
Module list
From src/core/src/models/mod.rs and the commands/repositories registered in lib.rs:
profilesuser_rolesuser_test_sessionsreading_tests,reading_passages,reading_question_groups,reading_questionswriting_tests,writing_taskslistening_tests,listening_sections,listening_question_groups,listening_questionspractice_library(read-only aggregation across the three test types)import(validation + import models shared across Reading/Writing/Listening)pagination(sharedPaginatedResponse<T>shape)
Additional non-model modules: commands::grade_writing (AI grading), commands::storage (media uploads), commands::export (ZIP export).
Error handling flow
src/core/src/error.rs defines a single AppError enum via thiserror:
#[derive(Debug, Error)]
pub enum AppError {
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
#[error("migration error: {0}")]
Migration(#[from] sqlx::migrate::MigrateError),
#[error("not found: {0}")]
NotFound(String),
#[error("validation error: {0}")]
Validation(String),
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
}
impl From<AppError> for String {
fn from(e: AppError) -> String {
e.to_string()
}
}
Commands return Result<T, String> (or Result<T, AppError> that is converted .map_err(Into::into) / via ? at the command boundary), because Tauri command errors must be serializable — String is the simplest shape for the frontend to catch and display as a toast. See Error Handling Conventions for the exact strings to expect.
Pattern for adding a new feature
- Define the model struct(s) in
src/core/src/models/<domain>.rs. - Implement repository functions (all SQL + ownership/auth checks) in
src/core/src/repositories/<domain>.rs. - Add thin
#[tauri::command]functions insrc/core/src/commands/<domain>.rsthat validate input and delegate to the repository. - Register each new command in the
tauri::generate_handler![...]list insrc/core/src/lib.rs. - Add a typed wrapper function in
src/ui/lib/tauri.ts— this is the only place the frontend is allowed to callinvoke().
See Adding a New Feature End-to-End for a full worked example using the real export_test_to_zip feature.
:::warning Do not build Rust manually
Do not run cargo build/cargo run to produce the app binary — Tauri tooling (npm run tauri / npm run tauri:build) handles compiling and linking Rust with the frontend automatically. Standalone cargo test/cargo clippy from src/core/ are fine for backend-only iteration.
:::