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