Detailed module structure, data flow, and patterns for the Template project.
┌─────────────────────────────────────────────────────┐
│ composeApp │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ app:auth │ │app:admin │ │app:dash │ ... │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ └─────────────┼───────────┘ │
│ ▼ │
│ app:designsystem │
└─────────────────────┬───────────────────────────────┘
│
┌──────▼──────┐
│ core:sdk │ ← HTTP client (Ktor)
└──────┬──────┘
│ network
┌──────▼──────┐
│ shared │ ← DTOs, request/response models
└──────┬──────┘
│
┌─────────────────────▼───────────────────────────────┐
│ server │
│ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │server:auth │ │server:groups│ │ server:files │ │
│ └─────┬──────┘ └──────┬─────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌─────▼───────────────▼──────────────▼───────┐ │
│ │ server:core │ │
│ │ ┌────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ config │ │ database │ │ security │ │ │
│ │ └────────┘ └──────────┘ └──────────┘ │ │
│ └────────────────────────────────────────────┘ │
│ │
│ ┌────────────┐ │
│ │ server:ai │ (agents, RAG, vector search) │
│ └────────────┘ │
└──────────────────────────────────────────────────────┘
Shared core modules (used by both client and server):
core:models — Domain models
core:mvi — MVI ViewModel base classes
core:testing — Test utilities and DSL
core:storage — Multiplatform key-value storage
User Action
→ ViewModel (MVI: Intent → Reduce → Effect)
→ SDK method (core:sdk)
→ Ktor HTTP Client
→ Ktor Server Route
→ Service layer (business logic)
→ Repository (Exposed R2DBC)
→ PostgreSQL
LoginScreen → LoginViewModel.send(LoginIntent.Submit)
→ LoginViewModel.reduce() → calls SDK
→ AuthApi.login(email, password)
→ POST /api/auth/login (Ktor client)
→ authRoutes() in server:auth
→ AuthService.login()
→ UserRepository.findByEmail()
→ UsersTable SELECT via R2DBC
| Module | Responsibility | Key Classes |
|---|---|---|
server:auth |
JWT authentication, OAuth (Google/Apple), password reset, user CRUD | AuthService, UserRepository, OAuthService, PasswordResetService |
server:groups |
Group CRUD, membership management, invitation system | GroupService, InvitationService, MembershipRepository |
server:files |
S3/MinIO file upload and retrieval | FileService, S3Client |
server:ai |
AI agents (chat, assistant), RAG pipeline, document ingestion | ChatAgentService, AssistantAgentService, DocumentIngestionService |
server:core:config |
Environment configuration, dotenv loading | Env, Configuration |
server:core:database |
R2DBC database setup, migration runner, vector column types | startDatabase(), MigrationRegistry, VectorColumnType |
server:core:security |
JWT validation, authentication plugin configuration | configureSecurity() |
| Module | Responsibility |
|---|---|
app:auth |
Login, registration, password reset screens |
app:admin |
Admin panel — user management, invitations table |
app:dashboard |
Main dashboard screen |
app:documents |
Document management UI for RAG pipeline |
app:profile |
User profile, avatar upload |
app:designsystem |
Shared UI components, theme, typography |
| Module | Responsibility |
|---|---|
core:models |
Domain models shared across client and server |
core:sdk |
API client — typed HTTP methods for all server endpoints |
core:mvi |
MviViewModel base class, test DSL |
core:testing |
Shared test utilities |
core:storage |
Multiplatform key-value storage (settings) |
All environment variables are defined in server/core/config/src/main/kotlin/com/m2f/core/config/configuration/Env.kt and documented in .env.example.
| Service | Container Name | Internal Port | External Port |
|---|---|---|---|
| PostgreSQL (pgvector) | template-postgres |
5432 | 5436 |
| MinIO (S3) | template-minio |
9000 / 9001 | 9002 / 9003 |
| MailHog (SMTP) | template-mailhog |
1025 / 8025 | 1025 / 8025 |
Credentials: PostgreSQL postgres/postgres, MinIO minioadmin/minioadmin
Server modules use Kotlin context parameters to inject shared dependencies:
context(config: Configuration, database: R2dbcDatabase)
fun Application.module() { ... }This pattern threads Configuration and R2dbcDatabase through the application without global state.
Each server feature module defines a Koin module for its services and repositories:
val authModule = module {
single { UserRepository(get()) }
single { AuthService(get(), get()) }
}Modules are composed in server/src/main/kotlin/.../di/ServerModule.kt.
Each server feature module follows a consistent structure:
server/<feature>/
├── build.gradle.kts — Uses server-module-convention plugin
├── src/main/kotlin/.../
│ ├── routes/ — Ktor route definitions
│ ├── service/ — Business logic
│ ├── repository/ — Database access (Exposed R2DBC)
│ ├── tables/ — Exposed Table definitions
│ ├── models/ — Module-specific data classes
│ └── migrations/ — Database migrations
└── src/test/kotlin/.../ — Tests
Services return Either<DomainError, Result> for typed error handling:
suspend fun login(email: String, password: String): Either<AuthError, TokenPair> = either {
val user = userRepository.findByEmail(email) ?: raise(AuthError.InvalidCredentials)
ensure(BCrypt.checkpw(password, user.passwordHash)) { AuthError.InvalidCredentials }
generateTokenPair(user)
}Client ViewModels extend MviViewModel<State, Intent, Effect>:
class LoginViewModel : MviViewModel<LoginState, LoginIntent, LoginEffect>(LoginState()) {
override fun reduce(intent: LoginIntent) { ... }
}See core:mvi for the base class and test DSL.
Migrations are registered via MigrationRegistry and run automatically on server startup:
fun registerAuthMigrations() {
MigrationRegistry.register(CreateUsersTableMigration())
MigrationRegistry.register(CreateRolesTableMigration())
}Follow the server:auth and server:groups modules as canonical examples.
server/<feature>/src/main/kotlin/com/m2f/server/<feature>/
plugins {
id("server-module-convention")
}
group = "com.m2f.server"
dependencies {
implementation(projects.server.core.config)
implementation(projects.server.core.database)
implementation(projects.server.core.security)
// Add feature-specific dependencies
}include("server:<feature>")Follow the route → service → repository layering pattern from existing modules.
fun register<Feature>Migrations() {
MigrationRegistry.register(Create<Feature>TableMigration())
}Call this in Application.kt main function before startDatabase().
Define a Koin module with your services and repositories, add it to serverModule.
routing {
val service: <Feature>Service by inject()
<feature>Routes(service)
}If the client needs to consume this feature, add request/response models in shared/.