Split shared expenses with friends, flatmates and travel groups —
one Kotlin codebase running on Android, iOS, Desktop and Web.
▶ Try it in your browser — app.tabmates.de
TabMates is a Kotlin Multiplatform (KMP) + Compose Multiplatform (CMP) app for tracking who paid for what in a group and settling up. The UI, business logic and data layer are shared across all targets; only thin platform shells differ.
- Groups — create groups, invite members via share links, join by link.
- Expenses — add, edit and view expenses with flexible splitting and multiple currencies per expense.
- Offline-first — browse groups and expenses and keep working without a connection; changes sync once you're back online (local Room cache).
- Settle up — see balances and who owes whom.
- Activity feed — recent changes across your groups.
- Accounts — register, log in, continue as guest, email verification and password reset.
- Push notifications — group activity via Firebase Cloud Messaging (Android/iOS), a WebSocket-driven channel on Desktop, and the Firebase JS SDK on Web. See
features/notifications/README.md. - App updates — update check against the backend with optional and forced update prompts.
- Settings — theme (light/dark/system), in-app language, notification toggle with permission handling, and an open-source licenses screen.
| Target | Shell |
|---|---|
| Android | :androidApp |
| iOS | iosApp (SwiftUI host) |
| Desktop (JVM) | :composeApp desktop entry |
| Web (WasmJS) | :composeApp web entry — installable, offline-capable PWA |
- JDK 17+
- Android Studio (latest stable) / IntelliJ IDEA
- Xcode (for iOS), on macOS
Build-time config is injected via BuildKonfig and required to build. Add these to a local.properties file in the repo root (git-ignored), or provide them as environment variables in CI:
API_KEY=your-api-key
BASE_URL_HTTP=https://your-backend.example.com
BASE_URL_PUBLIC=https://your-web-app.example.comThere is no separate websocket URL: it is derived from BASE_URL_HTTP by swapping the scheme
(https → wss, http → ws) and appending /ws.
BASE_URL_PUBLIC is the user-facing host for shareable links and deep links (App Links / web fallback), decoupled from the API host. For local dev, set it to your BASE_URL_HTTP value. See docs/WEB_DEPLOYMENT.md.
Optional per-target overrides let every target run against a local backend (http://localhost:8080) at the same time — the Android emulator reaches the host via 10.0.2.2, and the browser stays same-origin through the webpack dev proxy:
BASE_URL_HTTP_ANDROID=http://10.0.2.2:8080
BASE_URL_HTTP_WEB=http://localhost:8081
# Optional, web-only: Cloudflare Turnstile site key (a public identifier) for the invisible bot
# check on the auth endpoints. Unset = no widget/token; harmless until the backend enforces it.
TURNSTILE_SITE_KEY=your-site-key
# Optional, web-only: Firebase Web Push certificate (VAPID) key for FCM push notifications.
# Unset = no push token requested; see features/notifications/README.md.
FCM_VAPID_KEY=your-vapid-key
# Optional, native-only: per-release token for the backend's client-version gate. Release CI mints
# it from a secret this repo never contains; for local dev against a dev backend, mint it yourself:
# printf 'desktop|0.0.24' | openssl dgst -sha256 -hmac dev-client-secret -binary \
# | basenc --base64url | tr -d '='
# The platform and version must match the build you are running (see APP_VERSION), or the backend
# answers 426. Unset = no token; harmless until the backend enables the gate.
CLIENT_BUILD_TOKEN=your-build-tokenThe web dev server always proxies /api and /ws to the backend (composeApp/webpack.config.d/proxy.js) because the backend serves no CORS headers; the proxy target follows the active BASE_URL_HTTP, so switching it points the web dev build at that environment too.
Push notifications additionally need Firebase config — see features/notifications/README.md. A dummy androidApp/google-services.json is committed so the project builds out of the box; replace it with a real one for working push.
# Android — install on a device/emulator (or just run :androidApp from the IDE)
./gradlew :androidApp:installDebug
# Desktop (JVM), hot-reload enabled
./gradlew :composeApp:hotRunDesktop
# Web (WasmJS) — dev server on http://localhost:8081 (8080 is the backend dev server)
./gradlew :composeApp:wasmJsBrowserDevelopmentRuniOS: open iosApp/iosApp.xcodeproj in Xcode and run, or use the KMP/AndroidStudio run configuration.
./gradlew build # build all targets
./gradlew check # run tests + lint (ktlint)
./gradlew ktlintFormat # auto-format- Kotlin Multiplatform + Compose Multiplatform UI
- Clean Architecture + MVI/MVVM presentation
- Koin (annotations + compiler) for dependency injection
- Navigation 3 (type-safe routes)
- Ktor client (ContentNegotiation + WebSockets) for networking
- Room (KMP) for local persistence
- kotlinx.serialization, KSafe (encrypted storage), BuildKonfig (build-time config)
- kmpnotifier / Firebase for push, AboutLibraries for license attribution
- Gradle convention plugins in
build-logic, version catalog ingradle/libs.versions.toml
The project is modularized by feature and by layer. Dependencies point inward: presentation → domain ← data, with core shared by all features.
flowchart TD
A[":androidApp · iosApp"] --> B
B[":composeApp<br/>DI + navigation, platform entry points"] --> C
C[":features:*<br/>presentation → domain ← data"] --> D
D[":core:*<br/>domain · data · presentation · designsystem"]
:core:domain— pure Kotlin: models,Result<D, E>, error types, logging.:core:data— shared networking (HttpClientFactory), encrypted storage (SecureStore), preferences.:core:presentation— shared UI utilities (UiText,ObserveAsEvents, navigation contracts).:core:designsystem— theme, tokens and reusable Compose components.:features:*— each feature split intodomain(interfaces, models),data(implementations, DTOs, mappers),presentation(screens, ViewModels, routes), optionaldatabase/testing.:composeApp— shared entry point: wires navigation and DI, hosts Desktop/Webmainand the iOSMainViewController.:androidApp— Android application shell.
composeApp/ Shared app entry (DI + navigation), desktop & web main, iOS controller
androidApp/ Android application
iosApp/ iOS SwiftUI host (Xcode project)
core/
data/ domain/ presentation/ designsystem/
features/
appupdate/ data · domain
authentication/ data · domain · presentation · testing
notifications/ data · domain · testing
tabgroup/ data · domain · presentation · database · sqliteWasmWorker
build-logic/ Gradle convention plugins
gradle/libs.versions.toml Version catalog
See AGENTS.md for detailed conventions.
Hosting the web (WasmJS) build in production has extra requirements: cross-origin isolation headers (COOP/COEP) for the OPFS-backed database, a Content-Security-Policy, and CORS / websocket-origin configuration on the backend. The GitHub Pages deploy workflow (.github/workflows/deploy-web.yml) handles the build and publishing. Everything is documented in docs/WEB_DEPLOYMENT.md.
AGENTS.md— architecture, patterns and conventions in depthdocs/WEB_DEPLOYMENT.md— web hosting, CSP, CORS, GitHub Pagesfeatures/notifications/README.md— push notification setup per platform
Licensed under the GNU General Public License v3.0. See LICENSE.









