Mimir Architecture
This document provides a high-level overview of the Mimir codebase architecture for developers.
Crate Overview
Mimir is organized as a Cargo workspace with 4 crates:
┌─────────────────────────────────────────────────────────┐
│ mimir (Main App) │
│ Tauri Desktop Application │
└─────────────────────────┬───────────────────────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ mimir-core │ │ mimir-mcp │ │ mimir-print │
│ Core Logic │ │ MCP Server │ │ PDF Export │
└───────────────┘ └───────────────┘ └───────────────┘
mimir-core
The heart of the system containing:
- Models: Domain types for campaigns, characters, D&D catalog data
- Services: Business logic (28 services)
- DAL: Data Access Layer with repository traits
- Migrations: 35 Diesel migrations for SQLite schema
mimir-mcp
MCP (Model Context Protocol) server for Claude Code integration:
- Campaign, module, character, and catalog tools
- Uses DAL directly for database access
mimir-print
PDF generation using Typst:
- Character sheets
- Campaign documents
- Markdown to Typst conversion
mimir
Tauri desktop application:
- Commands: 50+ Tauri command handlers
- Services: LLM integration, context management
- State: Consolidated
AppStatestruct
Request Flow
┌──────────┐ ┌─────────────┐ ┌─────────────┐ ┌──────────┐
│ Frontend │───▶│ Tauri │───▶│ Service │───▶│ Database │
│ (Vue 3) │ │ Command │ │ Layer │ │ (SQLite) │
└──────────┘ └─────────────┘ └─────────────┘ └──────────┘
▲ │ │
│ ▼ │
│ ┌─────────────┐ │
└─────────│ AppState │◀──────────┘
│ (db, paths) │
└─────────────┘
Typical Command Flow
- Frontend invokes Tauri command (e.g.,
get_campaign) - Tauri Command extracts
State<AppState>and parameters - Service performs business logic, accesses database
- Response returned to frontend as JSON
Example:
#![allow(unused)]
fn main() {
#[tauri::command]
pub async fn get_campaign(
campaign_id: i32,
state: State<'_, AppState>,
) -> Result<ApiResponse<Campaign>, ApiError> {
let mut conn = state.db.get_connection()?;
let campaign = CampaignService::get_by_id(&mut conn, campaign_id)?;
Ok(ApiResponse::success(campaign))
}
}
Database Design
See ADR-0001 for denormalized design rationale.
Key Design Decisions
- Denormalized: Child entities (subclasses, subraces) contain full parent data
- JSON Columns: Complex/variable data stored as JSON (see ADR-0003)
- Multi-Ruleset: Content organized by rule system and source (see ADR-0002)
Schema Overview (37 tables)
Campaign Management:
campaigns,modules,sessionsdocuments,template_documentsworkflow_cards
D&D Catalog:
catalog_*tables for each entity type (spells, monsters, items, etc.)uploaded_books,catalog_sources
Characters:
players,characters,character_versionscampaign_players(links players to campaigns)
Service Layer
Services follow the stateful pattern per ADR-0005:
#![allow(unused)]
fn main() {
pub struct SpellService<'a> {
pub conn: &'a mut SqliteConnection,
}
impl<'a> SpellService<'a> {
pub fn new(conn: &'a mut SqliteConnection) -> Self {
Self { conn }
}
pub fn search(&mut self, filters: SpellFilters) -> Result<Vec<SpellSummary>> {
// Query logic
}
}
}
Service Categories
| Category | Services | Purpose |
|---|---|---|
| Campaign | CampaignService, ModuleService, SessionService | Campaign management |
| Catalog | SpellService, MonsterService, ItemService, etc. | D&D reference data |
| Character | CharacterService, CharacterCreationService | PC/NPC management |
| Content | DocumentService, TemplateService | Document management |
Key Patterns
Error Handling
#![allow(unused)]
fn main() {
// Application-level errors
pub enum MimirError {
Database(DbError),
Print(String),
Llm(String),
NotFound(String),
// ...
}
// Database-level errors
pub enum DbError {
NotFound { entity_type, id },
ConstraintViolation { field, message },
Query(diesel::result::Error),
// ...
}
}
State Management
AppState consolidates all shared state:
#![allow(unused)]
fn main() {
pub struct AppState {
pub db: Arc<DatabaseService>, // Database connection pool
pub paths: Arc<AppPaths>, // Application paths
}
}
Directory Structure
crates/
├── mimir/ # Main Tauri app
│ ├── src/
│ │ ├── main.rs # Entry point
│ │ ├── state.rs # AppState
│ │ ├── commands/ # Tauri commands
│ │ │ ├── catalog/ # D&D reference commands
│ │ │ ├── campaign/ # Campaign management
│ │ │ ├── character/ # Character operations
│ │ │ └── chat/ # LLM chat commands
│ │ └── services/ # Business logic
│ │ ├── llm/ # LLM integration
│ │ └── tools/ # LLM tool implementations
│ └── frontend/ # Vue 3 frontend
│
├── mimir-core/ # Core business logic
│ ├── src/
│ │ ├── models/ # Domain models
│ │ │ ├── catalog/ # D&D entity types
│ │ │ └── campaign/ # Campaign types
│ │ ├── services/ # Business logic services
│ │ ├── dal/ # Data access layer
│ │ └── migrations/ # Database migrations
│
├── mimir-mcp/ # MCP server for Claude Code
│ ├── src/
│ │ ├── context.rs # Database context
│ │ └── tools/ # MCP tool handlers
│ └── plugin/ # Claude Code plugin definition
│
└── mimir-print/ # PDF generation
└── src/
└── sections/ # PDF section renderers
Development Guidelines
Adding a New Catalog Entity
- Create model in
mimir-core/src/models/catalog/ - Add migration in
mimir-core/migrations/ - Create service in
mimir-core/src/services/ - Add Tauri commands in
mimir/src/commands/catalog/ - Update schema.rs after migration
Testing
# Run all tests
cargo test --workspace
# Run core tests only
cargo test -p mimir-core
# Run specific test
cargo test test_name