Rukon
HR Project Overview
iOS / MobileSeptember – October 2026 (Web deployed; iOS simulator & Android local APK)Personal Project Case Study Ready
SpenDrop

SpenDrop

SpenDrop: tri-platform personal finance and on-device receipt intelligence (iOS, Android, Web)

Core Value Proposition

A tri-platform personal finance system engineered for sub-millisecond persistence (0.240 ms) and on-device receipt OCR across iOS, Android, and the Web.

A cross-platform personal finance ecosystem spanning native iOS (SwiftData, Apple Vision OCR), native Android (Kotlin, Jetpack Compose, Room SQLite, Google ML Kit), and a Next.js 15 Web PWA. Share a payment confirmation from Touch 'n Go, a bank app, or Apple Pay, or pick a receipt photo, and SpenDrop extracts financial metadata on-device before confirmation. Features a local-first persistent outbox with verified 0.240 ms save latency, Hybrid Split (combining individual fixed allocations with automatic remainder distribution), integer-sen exact arithmetic, and an evaluation corpus of 9,811 test cases. Local-first with optional Supabase cloud sync. Personal project; verified on iOS simulator, Android hardware (OPPO A57), and web production.

Three iPhone screenshots of SpenDrop: the dashboard with today, week and month totals; the iOS share sheet with SpenDrop in the app row; and the Share Extension's review form showing Payment Detected, RM 18.50, McDonald's, Food, Touch 'n Go.
Dashboard, the share sheet, and the extension's filled-in review. iPhone 17 simulator, synthetic data.
Role
Sole Developer

Product Design, iOS Architecture, Android Jetpack Compose App, Web App, ML Kit/Vision OCR Engines, Tests

Context / Client
Personal project, built for everyday Malaysian payments and multi-platform parity
Timeline / Period
September – October 2026 (Web deployed; iOS simulator & Android local APK)
Category
iOS / Mobile

Key Highlights & Practical Impact

Executive Scan
  • Tri-platform parity: Native iOS (SwiftData/SwiftUI), native Android (Kotlin/Compose/Room), and Next.js 15 Web PWA
  • Sub-millisecond local persistence: 0.240 ms on-device save latency with background persistent outbox sync active (PR #5)
  • On-device OCR without cloud APIs: Apple Vision on iOS, Google ML Kit on Android, covering Malaysian bank receipts and e-wallets
  • 9.8k AI evaluation corpus: 9,811 test cases across train, dev, holdout, and adversarial sets with an independent integer-sen math oracle (PR #4)
  • Hybrid Split: Combines group fixed, individual fixed, and automatic remainder distribution without sen rounding discrepancies
  • Separation of Funding Accounts from Payment Channels, with reconciliation merging Apple Pay taps with delayed bank debits
  • 274 in-app iOS tests, 298 Android JVM tests, and 52 Android UI/Robolectric tests passing; verified on physical OPPO A57
Technologies Used
SwiftSwiftUISwiftDataKotlinJetpack ComposeRoom SQLiteGoogle ML KitApple VisionNext.js 15Supabase
LEVEL 2 — DEEP TECHNICAL PROOF

Ready to explore the build?

A detailed look at the end-to-end architecture, process workflows, key engineering decisions, live production capabilities, challenges, and results.

Writing from this work

Offline Receipt OCR on Android: On-Device Google ML Kit Text Recognition with Jetpack Compose

How SpenDrop's native Android engine parses payment receipts and merchant summaries offline on device using Google ML Kit, Jetpack Compose, and Room SQLite without third-party cloud APIs.

Sub-Millisecond Saves: Designing a Local-First Outbox and Sync Engine in SwiftData

How SpenDrop achieved an on-device save latency of 0.240 ms in SwiftData while synchronising in the background with a remote Supabase PostgreSQL backend using the persistent outbox pattern.

Two Ways I Use Supabase Row Level Security: Deny-All Behind a Server, or Per-Owner Policies

Across five projects, RLS does one of two jobs: close the Data API while a trusted server does the work, or let Postgres enforce who owns each row. When to pick which.

Testing Without Paid APIs or Hardware, and the Places Fakes Lied

Fake LLM providers, a simulated label printer and an in-process Postgres keep my test suites free and offline. What each proved, and three places a fake fell short.

SwiftData Migrations That Can't Lose Data: VersionedSchema, Pre-Upgrade Copies and a Safe Mode

SwiftData's inferred migration silently dropped a table. How SpenDrop moved to VersionedSchemas, pre-upgrade copies and a safe mode that never deletes.

Google Sign-In on iOS Through Supabase, With PKCE and No Google SDK

Google sign-in on iOS through Supabase with ASWebAuthenticationSession and PKCE, no Google SDK or client secret, plus an identity bug RLS would reject.

Splitting RM 10 Three Ways: Integer Sen and the Largest-Remainder Method

Floating-point money drifts and naive splits lose a sen. Integer sen at one boundary and the largest-remainder method, so shares always add up.

Sorting Apple Vision Text Into Lines Without Crashing Swift's Sort

VNRecognizeTextRequest returns boxes, not lines. Sorting them top to bottom and left to right with a comparator Swift's sort can trust.

Telling the Sender Bank From the Recipient on a Malaysian Transfer Receipt

A CIMB to Maybank transfer receipt names both banks. Reading the receipt's structure to attribute the payment to the bank that actually sent it.

Defaults Are Claims: Removing Placeholder Numbers That Looked Like Measurements

A 75% tank, a 10 km/L economy, a hardcoded user id: defaults in DriveKeep that rendered as real data, what replaced them, and the same rule in SpenDrop.

Apple Pay Is Not a Bank: Payment Channels, Funding Accounts and Reconciling Double Records

One purchase can arrive as an Apple Pay tap and again as a bank record. Separating how you paid from where money came from, and merging the duplicate.

Spending, Cash Flow and Who Owes Whom: Modelling Shared Bills in a Personal Finance App

When a bill is shared, 'how much did I spend?' has three honest answers. SpenDrop's rules for spending, cash flow, refunds and balances.

Screens That Look Like Payments but Aren't: Parsing Malaysian Receipts Without Fake Expenses

Balance, credit-limit and reward screens all show RM amounts. How SpenDrop rejects them, and the Malaysian receipt formats two of my parsers read.

Logging Apple Pay Taps Automatically With an App Intent and a Wallet Automation

An App Intent lets the Shortcuts Wallet automation hand each Apple Pay purchase to SpenDrop. The intent, its duplicate guard, and what iOS lets apps see.

Building an iOS Share Extension That Reads a Screenshot and Saves to the App's Database

How SpenDrop's Share Extension loads an image from any app, stays inside the memory limit, runs Vision OCR and saves to the app's shared SwiftData store.

Testing an iOS App With an In-App Test Runner, Launch Flags and XCUITest

274 logic checks run inside the app from a launch flag, a final check proves no test opened the real database, and six XCUITest flows drive the UI.

Implemented, Available, Not Connected: Writing Honest Status Into Project Docs

"It exists in the repository" and "it runs in production" are different claims. The three-state status model I use across my projects, with real rows from each.

Generating an Xcode Project File From a Script, With Stable IDs

Both of my iOS apps write project.pbxproj from a script. How the object IDs are made, why hashing names beats a counter, and what it costs.

An Append-Only Cloud Backup for a Local-First iOS App

SpenDrop's optional cloud backup never overwrites: per-user Storage folders, no UPDATE policies, a content hash, and a restore that copies local data first.

The SwiftData Store I Configured Was Never the One Being Used

Renaming an iOS app opened an empty database. The App Group store URL I had configured had never worked; SwiftData's fallback had been doing the job.

Which RM Is the Payment? Classifying Every Amount on a Receipt Screenshot

A payment screenshot shows fees, balances and advert prices as well as the payment. How SpenDrop labels every RM value, using keywords and screen position.