AG
TR

// writing / May 5, 2026

Kotlin Multiplatform in practice: building Muhabbet across Android, iOS, and backend

Muhabbet is a privacy-first Kotlin messenger for Turkey. Compose Multiplatform on mobile, Spring Boot on the backend, a shared kotlinx.serialization wire protocol in the middle. Notes from building it solo.

Muhabbet (Turkish for conversation, intimate talk) is a privacy-first messaging platform built for Turkey as a domestic MVP. The stack is Spring Boot 4 plus Kotlin 2.3 on the backend, Compose Multiplatform on mobile (Android shipping, iOS compiling with several pieces still stubbed), PostgreSQL 16, Redis 7, MinIO for media, and FCM for push. The wire protocol is a shared WsMessage sealed class serialized with kotlinx.serialization JSON over WebSocket. Android and the backend share the same sealed class; if the server adds a new variant, both clients fail to compile until they handle it. The codebase has 364 tests (314 backend, 23 mobile, 27 shared) and has not yet shipped to end users.

The shared/ module is the core of the Kotlin-everywhere bet. It holds domain models, protocol shapes, validation rules, and DTOs. I keep Spring annotations and Compose imports out of it deliberately: the instant something needs Context or @Service, it stays in its respective runtime. One tension that took a few iterations to resolve is the backend domain ContentType enum versus the shared one. Both exist, with mapper extension functions (Message.toSharedMessage()) doing the conversion in one place. The shared module pulls in kotlinx.serialization annotations on every class, which would break the rule that the domain layer has no serialization framework dependency, so they stay separate.

For unreliable mobile networks I use an offline-first SQLDelight message queue. The repository layer is cache-first: the UI reads from the local DB, and network sync writes through. Messages composed offline are written to a PendingMessage table with a client-generated UUID that acts as an idempotency key when the WebSocket reconnects. Reconnect timing uses exponential backoff with jitter (1s, 2s, 4s, up to 30s) so large numbers of devices coming back online after an outage do not synchronize their reconnects.

On the iOS side: the expect/actual mechanism works well for UI-adjacent platform code (AudioPlayer, CameraPicker, PushTokenProvider, and so on), but several key pieces remain stubbed. APNs push, phone-number auth, voice calls, and end-to-end encryption are not yet wired on iOS. E2E encryption is scaffolded but disabled (traffic is plaintext under TLS). One concrete gotcha worth noting: KoinApplication composable starts a new Koin instance and crashes on Activity recreate with KoinApplicationAlreadyStartedException. The fix is GlobalContext.getOrNull() ?: startKoin { ... }. For KVKK compliance the app provides soft-delete with deleted_at (hard-delete would cascade through eight or more FK tables), phone-number SHA-256 hashing for contact sync so the server never sees raw numbers, and a privacy dashboard with one-tap UI surfaces for data export, account deletion, and visibility controls.

Repository: github.com/ahmetabdullahgultekin/Muhabbet. Source access is by request: ahmetabdullahgultekin@gmail.com.

  • kotlin
  • multiplatform
  • mobile
  • kvkk