Skip to content

// project

Doori

Offline-first mileage, travel & expense tracker on one Kotlin codebase across Android, iOS, Wear OS, watchOS & Desktop.

The brief

Doori is an original, fully-offline mileage / travel / expense tracker I designed and built end-to-end in Kotlin & Compose Multiplatform. It runs on Android, iOS, Wear OS, watchOS and Compose Desktop from one shared codebase, offline-first with a real Kotlin/Ktor backend built in and off by default, so the whole thing stays reproducible and reviewable. It's my reference implementation for the architecture I advocate at scale: strict module isolation, a real location engine, a policy/reimbursement layer and a durable submit-outbox, all over local data.

What shipped

  • 49-module clean architecture: 13 feature modules meeting only at the composition root.
  • Real location engine, reimbursement policy engine, durable submit-outbox, and an on-device AI assistant.
Kotlin MultiplatformCompose MultiplatformAndroidiOSWear OSwatchOSDesktopRoom (KMP)Koin
GitHub PaymentsLab-KMP (sibling KMP app) Install it 49 modules · 5 platforms · 159 tests
built onkmp-build-logickmp-toolkit
feedskmp-toolkit

// case study

The short version

The problem

A reimbursement claim is only as good as the distance it's built on, and raw GPS drifts in urban canyons, tunnels and behind OEM-throttled updates: a policy engine downstream can't approve a payout against a number that noisy.

The decision

Treat GPS as a noisy signal rather than ground truth: jitter suppression, spike detection to reject physically impossible fixes, IMU (accelerometer) fusion and device-tier-adaptive sampling feed a four-bucket distance accumulator, with a deterministic simulated-drive source so the whole pipeline is unit-testable without hardware.

The result

That same location engine now backs a real reimbursement-rate policy engine and a durable submit-outbox, shipping across all five targets from one Kotlin codebase, with a real Kotlin/Ktor server landed on top, sharing typed DTOs with the client, off by default.

5 platforms · one codebase

Watch the two-minute tour ↓
Evidence: That same location engine now backs a real reimbursement-rate policy engine and a durable submit-outbox, shipping across all five targets from one Kotlin codebase, with a real Kotlin/Ktor server landed on top, sharing typed DTOs with the client, off by default.

// guided tour

Two minutes, narrated

A storyboarded walkthrough of the real app — tap the speaker for the voiceover, or read along with the captions.

Doori — narrated product tour

// shipping

Where you can actually get it

Every line below is checkable. Add the repository in an F-Droid client and the app installs, signed with the same key its listing pins.

  • F-DroidLive in a self-hosted repository. 66 MB, signed, tagged NonFreeDep because location and on-device receipt OCR use Google Play Services components. open
  • GitHub ReleasesSigned APK per tag, for both the Play flavour and the Google-free one. open

// pipeline

The build that put it there

Read live from GitHub Actions and the F-Droid index. Not a badge: the actual runs, what shipped, and the certificate you can check the download against.

reading the pipeline

49

Gradle modules (36 local + 13 composed)

13

isolated feature modules

5

platforms · one codebase

0

backend calls by default, real Ktor server opt-in

// multiplatform

One codebase, every surface

The real screens (and, where it's live, the running build) per platform — not a mockup.

Android screen (1 of 5)

// in motion

Watch it run

Home & dashboard: live capture

// design notes

How it works

49-module clean architecture (36 local + 13 composed)

Thirteen feature modules that never depend on each other, meeting only at the :app composition root and wired with Koin. A shared commonMain core holds the design system, Room (KMP) + DataStore, and every check-in / hardware-event screen, with platform services behind expect/actual. Convention plugins from my own kmp-build-logic keep every module's build consistent.

Location engine

GPS is treated as a noisy signal: jitter suppression, spike detection to reject impossible fixes, a four-bucket distance accumulator, IMU (accelerometer) fusion and device-tier-adaptive sampling that trades battery against precision by hardware class. A deterministic simulated-drive source makes the whole engine unit-testable without hardware.

Policy & reimbursement engine

A reimbursement-rate engine computes a payout from configurable per-vehicle rate rules, and the approvals flow flags policy violations against those rules. This is the real expense-platform logic a live product needs, implemented entirely against local data rather than stubbed with a snackbar.

Durable submit-outbox

Submitting a track or voucher journals the intent locally and reconciles it deterministically, so a process kill mid-submit never loses a record or double-counts one. Repositories were written to look one implementation-swap away from a real API, and that bet paid off: a real Kotlin/Ktor `:server` module now exists, sharing `:contract` DTOs with the client so the wire format can't drift, with JWT auth guarding every route. Off by default behind one flag, so the offline guarantee above is unchanged until someone flips it.

Five targets, one snapshot model

Beyond Android and iOS phones, the same shared SurfaceSnapshot drives a Wear OS app, a watchOS SwiftUI app and a Compose Desktop window, plus Android Glance + iOS WidgetKit home-screen widgets and an iOS Live Activity / Dynamic Island for an in-progress trip. Each surface has its own design-system skinning but reads the identical shared state.

Offline AI assistant

A chat assistant grounded entirely in local Room data: trips, expenses, cards. Real chunked streaming (not a fake typing animation), persistent history with a 5-minute session-resume window, on-device speech-to-text/text-to-speech, and local usage analytics. No remote LLM, no server, same offline guarantee as the rest of the app.

Super-profile & plugin-composition platform (V24, shipped)

The newest depth wave: a single plugin registry is the app's composition mechanism. TILE / CAPABILITY / VALUE plugins resolve by layering FORCED > USER > PRESET > DEFAULT, editable live from a Master Plugin page with source chips. Four persona presets (Corporate Commuter, Super-App Consumer, Gig Driver, Minimal Guest) reshape hubs, auth flows, tracking behaviour and tunables from one account. Built on top: act-on-behalf session delegation with an app-wide "Acting as" banner, a verification centre with corporate-email/OTP + card KYC, growth surfaces (referral, coupons, scratch rewards), membership (club, subscriptions, incentives), external wallet linking via OTP, and payout identity (masked bank + editable UPI handle + QR). All shipped, with a V25→V37 series landed on top (on-device intelligence, JWT auth, closeout hardening, home cards/advances, What's New), still offline-first by default with the real backend opt-in.

Master search, dynamic forms and document intelligence

A registry-based master search fans one query across every feature module's own search provider from a single results screen, instead of a separate search box per hub. A dynamic form engine drives expense and claim entry: field validation, conditional visibility and GST auto-calc, with AI field suggestions fed by an on-device document-intelligence pipeline that combines on-device AI, text recognition and heuristics for OCR field-fill, doc-type classification and duplicate detection on a scanned receipt, degrading gracefully wherever a model isn't available.

FOSS-safe distribution & quality gates

Dual gms / noGms builds (Google Play + F-Droid) with a dependency-prefix guard that fails the build if proprietary libraries leak into the FOSS flavor. 159 Roborazzi JVM screenshot tests (no emulator, no network) covering phone, watch and desktop, plus Napier logging, detekt, ktlint, Kover and CI.

// architecture

How it's built

49-module architecture, features meet only at :app

Location pipeline: GPS treated as a noisy signal

One shared snapshot → five targets

// under the hood

Tech stack

Language & UI

  • Kotlin
  • Compose Multiplatform
  • Material 3
  • SwiftUI (watchOS)

Data

  • Room (KMP)
  • DataStore
  • Coroutines + Flow
  • Durable submit-outbox

Domain

  • Location engine (jitter · spike · IMU fusion)
  • Reimbursement-rate policy engine
  • Master search (provider registry)
  • Dynamic form engine (GST auto-calc, conditional visibility)
  • On-device document intelligence

Backend (opt-in, off by default)

  • Ktor + Exposed `:server`
  • shared `:contract` DTOs
  • JWT auth

DI & build

  • Koin
  • kmp-build-logic convention plugins
  • AGP
  • Gradle KTS

Maps & platform

  • MapLibre (F-Droid)
  • KrossMap (Play)
  • Glance + WidgetKit widgets
  • Live Activity / Dynamic Island

Quality

  • Roborazzi (159 JVM screenshot tests)
  • detekt
  • ktlint
  • Kover
  • CI

// directions

Same screen, 5 treatments

Pick a treatment to see it whole. Where one ships a dark counterpart, drag the divider to compare the two faces of it — same layout, same content, only the light changes.

direction approvals — ledger, light
ledgersingle mode — no dark counterpart

// gallery

Screens (63)

Swipe or use the arrows · tap a screen to enlarge

See how everything connects →

frequently asked

What does he do at Dice.tech?

SDE-2, Android & Product Owner at Dice.tech (June 2023 - Present), platform owner of the app behind 50k+ monthly active users (22k+ daily, platform owner at Dice.tech). See the source.

What did he do to improve GPS accuracy?

Took GPS accuracy to 95%: up from 50%, by predictive dead reckoning. See the source.

How did he reduce production crashes?

80% crash reduction: Crashlytics + structured concurrency fixes. See the source.

How much of the app is Jetpack Compose?

~87% of the UI layer: 455k of 523k UI-layer LOC, verified screen by screen against the legacy XML. See the source.

Where did he study?

B.Tech, Computer Science & Engineering, NIT Bhopal (MANIT) (2017 - 2021). See the source.

What is the Neev Consulting role?

Consulting Engineer, Platform & AI at Neev Consulting (April 2026 - Present). Built the LLM assistant layer of an ERPNext/Frappe consulting ERP: business-context resolution, capability discovery, and an AI capability gate that defaults OFF with a test proving it. Models client to project to PO to milestone to GST invoice to payment end to end. See the source.

What is Doori?

Doori: Offline-first mileage, travel & expense tracker on one Kotlin codebase across Android, iOS, Wear OS, watchOS & Desktop. See the source.

What is Gaddi?

Gaddi: A Hinglish social-deduction bluffing game of power, satire & second chances. Gaddi ke liye kuch bhi karega. See the source.

What is PaymentsLab-KMP?

PaymentsLab-KMP: An Integration Lab for the Android payments ecosystem: every gateway behind one abstraction, with a live look at what actually happens on each transaction. See the source.

What is Candidai?

Candidai: A native, multiplatform AI career-intelligence engine, and the open-source project it's built on. See the source.

Does he write, outside of code?

Yes: The Loopdown, his field-notes writing hub, where the recurring bug characters are named after real production incidents. See the source.

What's he like to work with?

Answered by his own teammates, not by him: see the EB Profiles, one question per member, in their own words. See the source.

Is he available, and how do I reach him?

Open to remote (worldwide / India) and hybrid in Pune / Bengaluru. Email siddharthpandalai990@gmail.com. See the source.

What has he contributed outside employer work?

Shared Kotlin Multiplatform libraries used across his own apps, plus merged upstream pull requests on career-ops: see the repos and the running count. See the source.

Has anything he's built shipped for real?

Yes: see the Play Store shelf for the apps that are actually live and installable, not just source. See the source.

What can I try on this site besides reading it?

Every route on the site, as a tile you can open: 3D builds, labs and canvases, not screenshots. See the source.