Imported from danmarg/where (
AGENTS.md). Install upstream withnpx skills add danmarg/where. Copyright stays with the author.
Where — Design Choices & Coding Standards
Project overview
Where is a cross-platform real-time location sharing app (iOS, Android, Ktor server) built with Kotlin Multiplatform (KMP). It uses end-to-end encryption (E2EE) by default.
Tech stack: Kotlin 2.1 (shared, Android, server, CLI), Swift 6 (iOS), Jetpack Compose (Android), SwiftUI (iOS), Ktor (Netty), kotlinx.serialization, Gradle version catalog.
Architecture
Module layout
| Module | Role |
|---|---|
shared/ |
KMP library — data models, E2EE crypto implementations (Double Ratchet), LocationClient / E2eeMailboxClient |
android/ |
Android app — Compose UI, FusedLocation foreground service, Google Maps |
server/ |
Ktor server — Anonymous Mailbox API, Redis-backed persistent mailbox store |
ios/ |
iOS app — SwiftUI + MapKit + CoreLocation, native HTTP polling |
cli/ |
Kotlin JVM CLI — utility tool for management and testing |
Data flow
[iOS / Android]
GPS → LocationService / LocationManager
→ LocationClient.sendLocation() (KMP on Android, Swift on iOS)
→ POST /inbox/{send_token}
→ recipient polls GET /inbox/{recv_token}
→ clients update map pins
Real-time transport
- Mailbox API on
/inbox/{token}(Ktor REST endpoints). - Server routes opaque encrypted payloads using pairwise routing tokens. To prevent clients from reading their own messages, each session uses separate
sendToken/recvTokenpairs symmetrically derived from the ratcheted root key using sender and recipient fingerprints. - Messages are encapsulated in standard Double Ratchet envelopes.
- Clients maintain post-compromise security (PCS) mathematically through automated Keepalives even if only one user is sharing location.
- Clients poll for updates at a constant rate.
User identity
- Identity is determined by the device key itself (ephemeral X25519 session keys).
- No stable random UUIDs or long-term identity keys.
- Fingerprints are derived from session-scoped ephemeral public keys.
Trust On First Use is NOT a bug; it's a choice (and accepted risk) of the current design.
E2EE
Uses a standard, bidirectional Double Ratchet protocol with X25519 ephemeral keys, HKDF-SHA-256 for ratcheting, and ChaCha20-Poly1305 for encryption. See docs/e2ee-location-sync.md for the full protocol spec.
Key design decisions
Shared vs. platform-specific networking
- Android: uses
LocationClientfrom the shared KMP module (Ktor HTTP client over OkHttp). - iOS: uses a native Swift implementation for HTTP polling/posting.
- All protocol data models are in the shared KMP module. iOS imports the
Sharedframework and uses these types directly.
Maps
- Android:
maps-compose(Google Maps Compose). Requires aMAPS_API_KEYinlocal.properties. - iOS:
MapKitviaUIViewRepresentable— no API key needed.
Battery efficiency
- Android:
FusedLocationProviderClientwithPRIORITY_BALANCED_POWER_ACCURACY, 30s interval, run inside a foreground service so the OS does not kill it. - iOS:
distanceFilter = 50m+desiredAccuracy = kCLLocationAccuracyHundredMeters;startMonitoringSignificantLocationChanges()when backgrounded.
Server state
- Mailboxes are persisted in Redis. State survives restarts.
- Messages are retained for 7 days, aligning with the client re-pair timeout.
Coding standards
Kotlin (shared + server)
- Kotlin 2.1 / JVM 17 (Android) / JVM 21 (server/CLI).
kotlinx.serializationfor all JSON. Sealed classes useclassDiscriminator = "type"and@SerialNameon each subclass.- Coroutines: use
SupervisorJob()so one failed child doesn't cancel siblings. PreferStateFlowoverLiveData. - No mutable global state except the intentional
LocationRepositorysingleton (bridge between service and ViewModel).
Swift (iOS)
- Swift 6 strict concurrency — all
ObservableObjectclasses marked@MainActor. - Use
async/awaitfor async polling loops. - Native
URLSessionfor networking; no Ktor/coroutine bridging. - Use KMP types directly — no duplicate Swift structs.
Android
- Jetpack Compose only — no XML layouts.
- ViewModels use
AndroidViewModelwhenApplicationcontext is needed. LocationRepositoryis the single source of truth between the foregroundServiceandViewModel.
Dependency management
- All versions in
gradle/libs.versions.toml(version catalog). Never hardcode versions inline. - Add new dependencies to the catalog first, then reference via
libs.*alias.
Local development
Server
./gradlew :server:run # or ./scripts/run-server.sh
curl localhost:8080/health # → ok
Android (emulator)
- Server URL in
local.properties(or defaulthttp://10.0.2.2:8080). - Add your Maps API key:
echo "MAPS_API_KEY=your_key" >> local.properties - Build:
./gradlew :android:assembleDebugor./scripts/run-android.sh
iOS (simulator)
- Server URL in
ServerConfig.swift(defaulthttp://localhost:8080). - Generate Xcode project:
cd ios && xcodegen. The project callsembedAndSignAppleFrameworkForXcodeas a pre-build script automatically. - To build the KMP framework manually:
./gradlew :shared:embedAndSignAppleFrameworkForXcode - Run:
./scripts/run-ios.shor openWhere.xcodeprojin Xcode.
CLI
./gradlew :cli:run # or ./scripts/run-cli.sh
Local build configuration
Machine-specific paths (build output dirs, SDK locations, cache dirs) must never be added to checked-in files (gradle.properties, build.gradle.kts, etc.). Use gitignored local overrides instead:
local.gradle.kts(gitignored) — applied automatically by the rootbuild.gradle.ktsif present..envrc(gitignored) — use for env vars likeGRADLE_USER_HOME,KONAN_DATA_DIR,TMPDIR.local.properties(gitignored) — Android SDK path and other local Android properties.
The /Volumes/Ext external drive is used on the dev machine for all large caches and build outputs. These settings live in local.gradle.kts and .envrc, not in source control.
Running tests
./gradlew :shared:jvmTest # unit tests (requires JRE)
./e2e-test.sh # end-to-end integration tests
The E2EE crypto library tests live in shared/src/commonTest/kotlin/net/af0/where/e2ee/
and run on the JVM target via :shared:jvmTest.
Key files
gradle/libs.versions.toml— central dependency managementdocs/e2ee-location-sync.md— full cryptographic protocol specificationdocs/IMPLEMENTATION-CHECKLIST.md— E2EE implementation status and roadmap
Planned future work
- User-controlled sharing (groups, time-limited sharing)
- Push notifications when a friend's location changes significantly