Skip to content

Architecture

Lwb89dev edited this page Oct 19, 2026 · 1 revision

Architecture

In one paragraph

Roadstr 1.x is a native Kotlin app — Jetpack Compose for the interface, MapLibre Native for the map. Pure logic (the Nostr protocol, routing and search protocols, navigation) is kept free of Android so it can be tested on a plain JVM; Android-specific I/O sits in thin services. A Flutter engine is still packaged, for one reason only: on the first start of 1.x it reads the previous Flutter app's data once, so an update loses nothing.

Repository layout

android/app/src/main/kotlin/app/roadstr/   # the app (Kotlin, Jetpack Compose)
├── core/          # pure logic with no Android dependency: Nostr protocol (NIP-01/04/19/44/46/47/57/58/78, BIP-340),
│                  # the road-report interoperability codec and MeshCore companion protocol (core/protocol/interop,
│                  # core/protocol/meshcore), routing and search protocols, navigation phrases, discovery (Explore),
│                  # journey and achievements, speech (model contract, end-of-speech detector, address repair)
├── feature/       # one package per screen or flow: home, map, navigation (HUD, host), onboarding, place, profile,
│                  # report, route, saved, savedroute, savedtransit, offline, search, settings, transit, performance,
│                  # voice, speech, web, wikipedia, activity, history, discovery, journey
├── service/       # I/O: Nostr relays, remote signer (bunker), zaps and NWC, MeshCore radio and bridge, routing, search,
│                  # public transport (Transitous, Entur), offline packages, location, hazards, notifications, the
│                  # navigation foreground service, the speech models and microphone
├── offlinestt/    # the Kotlin API of the embedded speech engine (vendored)
├── storage/       # preferences, secrets, saved places, history, pending reports
├── migration/     # the reader of the old (Flutter) app's data, transactional and fail-closed
└── startup/       # the launcher activity, the one-time profile import, the guidance runtime holder
android/app/src/test/                      # JVM tests of all of the above (about 1,500 cases)
native-android/                            # the road-test app (same shell, own stores) and the optional GeckoView variant
lib/                                       # the Flutter code of 0.5.x, kept for the first-start import and the shared translations
test/                                      # Dart contract tests that pin generated resources, the build and the docs
tools/kotlin_rewrite/                      # generators for Android string resources and test fixtures, release audit
tools/interop/                             # independent generator of the road-report test vectors (stdlib-only Python)
third_party/                               # vendored code and binaries, each with a record and a rebuild path
docs/                                      # plans, decisions, risks and release notes

Why GitHub shows roughly half Dart and half Kotlin. The language bar counts the bytes of every tracked file by language; it does not know which code ships. The repository still holds the whole Flutter app (about 1.9 MB), because it reads the previous version's data and shares the translation files, plus about 1.5 MB of generated Dart localisation classes, the Dart test suite and the Dart tools that generate the Android resources. The Kotlin share is the app itself plus its tests.

Principles that shape the code

  • Pure core, thin services. core/ has no Android types. Protocols are validated with golden vectors that are shared between languages and generated by independent implementations.
  • Fail closed. The first-start import is transactional: it copies before it opens, changes nothing in the old files, and offers Try again or Continue without importing — never a silently empty profile. Downloads are accepted only after size and SHA-256 match; packages are accepted only if they actually open.
  • Bounded everything. One HTTP client for all services with a deadline and a response-size limit; parsers cap their input; hostile relays, mirrors, packages and models are expected.
  • No hidden background work. Offline packages, voice models and the speech model are downloaded only on an explicit action, resume instead of restarting, and never from a periodic worker.
  • Say it in the UI. Anything that sends data to a third party is disclosed where it happens, and the disclosure is pinned by a test.
  • Reproducible binaries. Native binaries that are committed rebuild byte for byte from pinned sources, and the Gradle build refuses to start if one differs from its recorded checksum.

The navigation runtime

The trip, the voice and the GPS live outside the screen, held by a foreground service, so locking the phone, rotating it or swiping the app away does not end a trip. Road guidance is held in memory if Android kills the process; active public-transport guidance has encrypted recovery. The map follows the direction of travel, with the vehicle cursor animated from a continuous follow loop rather than a fixed tween.

Subsystems

Subsystem Notes
Map MapLibre Native. Dark themes recolour the standard tiles on the device. Offline maps are local PMTiles sources owned by the package manager, not MapLibre offline packs. See rendering decision
Routing OSRM, GraphHopper and OpenRouteService behind one contract; Valhalla as an opt-in on-device engine. Saved routes are an encrypted, versioned value with their own Keystore alias
Search Nominatim and Overpass, a deterministic multilingual parser for plain-language queries, a discovery layer for Explore
Nostr Relays, signer (Amber via NIP-55, bunkers via NIP-46), zaps and NWC, NIP-44 encrypted sync, a NIP-58 client
Voice Kokoro-82M and Piper on ONNX Runtime with eSpeak NG phonemisation (JNI)
Voice search transcribe.cpp (ggml) through a small JNI facade; Parakeet TDT 0.6B v3; two CPU builds chosen at run time by a feature probe
Public transport Transitous (MOTIS v6 plan API) and Entur (GraphQL), provider-scoped IDs, encrypted saved journeys
Radio MeshCore companion protocol over Bluetooth LE, a bridge with loop prevention and a rate limiter

Why the rewrite, and how it was kept safe

The Flutter app stayed in place as the production oracle while the native core grew: forensic baseline, native skeleton, migration feasibility, pure-core parity, services, UI parity, release parity — each gated on evidence. The documents are in docs/kotlin-rewrite/; the risks, one by one, are in RISKS.md and FEATURE_PARITY.md. The Flutter 0.5.x line lives on the flutter-maintenance branch.

Decision logs

Log Covers
docs/offline-engine/DECISIONS.md Offline packages, saved routes, Valhalla (OFF-001 …)
docs/world-discovery/DECISIONS.md Plain-language search, web results, the in-app browser (D-01 …)
docs/rendering-engine-decision.md Why MapLibre, and what was ruled out
docs/kotlin-rewrite/ The migration, parity and release tooling

Licences

Roadstr's own code is MIT. Third-party components keep their own licences, listed in THIRD_PARTY_NOTICES. Notably:

  • eSpeak NG (voice phonemisation) is GPL-3.0-or-later; because the APK binary includes it, a distributed APK has to honour the GPL for that component. Roadstr's source files stay MIT.
  • GeckoView (only in the optional in-app browser variant) is MPL-2.0.
  • The Parakeet speech model is downloaded on request and licensed CC-BY-4.0; its attribution is shown in Settings.
  • GeoNames data used for the offline catalogue is CC BY 4.0.

Clone this wiki locally