Building an offline-first travel app in .NET MAUI (on-device OCR, currency & maps, no backend)
A build note from Horizon Software , a one-person Android studio. WanderWallet is a travel budget app, and the whole thing runs on the phone: no account, no backend, no cloud. Here's how the parts that look like they need a server actually work without one. The one constraint that shaped everything WanderWallet has a single non-negotiable rule: it has to work with no signal. You're three countries into a trip, your phone's in airplane mode to dodge roaming charges, and you still need to know whether you're on budget. That one requirement quietly makes most of the architectural decisions for you — no login, no server round-trips, and every feature that would normally lean on a cloud API has to earn its keep another way. The stack is deliberately boring: .NET MAUI (Android-first), CommunityToolkit.Mvvm , sqlite-net-pcl for storage, and SkiaSharp for anything I draw myself. Everything the app records lives in a local SQLite database on the device and nowhere else. "Backup" is a file you export and keep — there's no server to back up to . The three features people assume need a backend turned out to be the most interesting to build, precisely because they don't. 1. Currency conversion that survives airplane mode A travel budget app that can't convert currencies offline is useless at exactly the moment you need it. So rates aren't fetched on demand. Whenever the app happens to have a connection it refreshes exchange rates for ~155 currencies and caches the whole table locally . From then on every conversion is local arithmetic — a connection only ever buys you a fresher table, never the ability to convert. The design decision that took me longest to get right: capture the conversion immutably, at entry time. Each expense stores the original amount, its original currency, the converted home-currency amount, and the exact rate used — and that rate is never recalculated: public class Expense { public double Amount { get ; set ; } // in OriginalCurrency public string Origina