Deep dive2026
Roamkit logo

Roamkit

A travel toolkit that keeps working when your signal does not.

Role
Solo: product, design and mobile
Ten tools, all of them working in airplane mode.

Roamkit is an offline-first travel app: one Expo codebase shipping to iOS, Android, and the web, packed with the tools you actually reach for abroad. Currency and unit converters, tip and split, a world clock, a phrasebook that speaks, a real LED flashlight with SOS, sun times, a compass, a trip budget, and a 57-article survival guide. Every single one works in airplane mode, which is the entire point.

Roamkit is built with Expo and Expo Router, React Native 0.85, and React 19. The home screen is a grid of tools driven by a single catalog file, so adding a tool is a data entry plus a route. A dark navy design system with per-tool identity colors keeps it feeling like one product instead of eleven small ones.

Monetization is a one-time 4.99 Pro unlock through RevenueCat: no subscription, no ads, no tracking. The app is live on the Google Play Store with real billing enabled, after a 14-day closed beta and production review.

Travel utilities fail exactly when you need them: at customs with no SIM, in a mountain town with one bar, on a plane. Most currency apps are a thin skin over an API call, so offline they show you a spinner and an apology.

The requirement was strict: every tool must produce a correct, honest answer with zero connectivity, and clearly label how fresh its data is.

The heart of the app is the rate store, a three-tier fallback for exchange rates. Tier one is a bundled snapshot that ships inside the JS bundle itself, so conversion works on the very first launch with the network off. Tier two is the last successful fetch, cached on-device. Tier three is a live refresh from a keyless public rate API when the app is online and the data is stale.

Reads never touch the network: the store returns the best available table instantly (memory, then cache, then bundled), and refreshes happen silently in the background. Concurrent screens share one in-flight request, and staleness is defined as older than 12 hours or past the feed's own advertised next-update time. The footer always tells the truth: live rates, saved rates, or built-in rates as of a date.

System map
Expo appiOS + Android + webTool grid10 tools + survival guideDevice APIstorch, GPS, TTS, sensorsRevenueCat$4.99 one-timeRate storethree-tier fallbackBundled snapshotships in the appAsyncStorage cachelast known ratesRate feedopen.er-api.com
drag to exploreReads never wait on the network; refresh happens quietly in the background.

Expo earns its keep here because the tools lean hard on device hardware through one API surface: the flashlight drives the actual LED torch through the camera module, the compass reads the OS fused heading with a raw magnetometer fallback, sun times come from GPS plus pure astronomy math, and the phrasebook speaks through on-device text-to-speech. No servers, no accounts, no backend at all.

Offline needs no cleverness for most tools: unit conversion is ratio math, the world clock uses the IANA timezone database already baked into the JavaScript engine, phrases are a static array, and sunrise math is an almanac formula. The only tool that genuinely needs the internet sometimes is currency, which is why it got the three-tier treatment.

Currency converts and compares 30 curated currencies, expandable to 162 with Pro. Tip and split has a round-up-per-person mode that folds the rounding back into the tip so the rows still sum, because a split that does not add up is a bar argument waiting to happen. The flashlight has light, strobe, and SOS modes, and the SOS is real ITU Morse timing: 220 millisecond units, three-unit dashes, hand-sequenced dots and dashes.

The phrasebook covers 20 essential phrases in 9 languages with tap-to-copy and tap-to-hear. The survival guide is 57 plainly written offline articles with a stop-word-stripping search, including such timeless advice as not chasing the snake that just bit you for a photo.

Offline first launch: the currency screen mounts, the rate store finds no memory or cache, normalizes the bundled snapshot, and converts through a USD pivot: amount divided by the source rate, times the target rate. A locale-tolerant parser handles both comma and dot decimals, because Android keyboards enjoy chaos, and an earlier naive parse could be off by a factor of a thousand on money. That one got fixed with prejudice.

Online, the same mount triggers a silent refresh: if the table is stale, one 8-second-capped fetch updates memory and cache, results recompute live, and the footer flips to live rates. The trip budget tool reuses the exact same store and conversion path, so an expense logged in yen converts with the same honesty rules as the converter.

There is no account and no analytics, and the camera permission exists solely to switch on the torch: the store listing and permission copy both promise no photos, ever, and the code never opens a preview. Android permissions that Play flags as risky are explicitly blocked in the app config.

The Pro unlock is guarded by a three-way gate: billing must be enabled, a platform RevenueCat key must exist, and only then can a purchase grant the entitlement. On the static web build neither condition holds, so Pro simply cannot be granted there, even by hand-editing local storage. RevenueCat itself is lazy-loaded so development builds without native billing modules run untouched.

Cross-platform hardware is where the fun lives. The web has no LED, so the flashlight falls back to a max-brightness white screen. The compass on some devices throws on the fused heading API, so a raw magnetometer path smooths the vector, not the angle, because averaging angles across the 359-to-0 wrap does exciting things to a compass needle.

The bundled data is kept deliberately small: 30 rates in the bundle rather than all 162, with the full catalog resolved through a few kilobytes of name-and-flag metadata. And the SOS screen refuses to let the phone sleep, because a sleep timer killing your distress signal is the kind of bug you only get to ship once.

The store listing and the social posts needed a video, so the spot was written and cut in house. Twenty three seconds: a castaway on a life raft signals a passing ship with the app's own SOS flashlight, then kisses the coconut he has been talking to. The live action is one generated shot from Kling 3.0. Everything else, which turned out to be most of it, was authored in ffmpeg.

The generated footage was not usable as delivered. Sampling the sky on its own showed luma sitting flat at 40 for six seconds and then surging to 132 the moment the flashlight fires, which is a model lighting an entire night sky with a phone torch. On screen it read as lightning. The fix was to measure that curve at half second intervals, fit a piecewise brightness correction to its inverse, and run a twelve frame deflicker over the result. The largest frame to frame luma change dropped from 92 units to 7.5.

The SOS is authored rather than generated, because no video model produces real Morse. It is a timed brightness modulation over the corrected footage: dot one unit, dash three, one unit between symbols, three between letters. The off beats land at 33 to 44 luma, which is the scene's own moonlight baseline, so the light reads as switching off rather than merely dimming.

The generated audio was thrown away entirely. It contained a thunder crack triggered by the flashlight and a line of dialogue nobody asked for. The ship's horn that replaces it is synthesized: a 105 Hz fundamental with a second horn detuned to 107.5 for the slow beat real horns have, harmonics at 210, 315, 420 and 525 Hz, then two cascaded low passes and a three tap echo to model what a kilometre of open water does to a sound. Distance is a spectral problem rather than a volume one, and the measurable result is a 12 dB tilt toward the low end. Both music beds are synthesized the same way, so nothing in the spot carries a licence.

The end card composites the real app icon, the ten real tool icons lifted from the home screen, and the Play badge. Those icons needed a superellipse alpha mask rather than a rounded rectangle: the app's own icons are squircles, and a circular corner arc cuts across them and leaves slivers of card background showing. The finished cut is mastered to -14 LUFS with a 4.7 LU range, so it plays at the same level as everything else in a feed instead of arriving quiet.

One generated shot, then a lot of ffmpeg: corrected lighting, authored Morse, a synthesized ship's horn and score.

Roamkit shipped: it is live on the Google Play Store with ten tools plus the survival guide, a coherent design system, real RevenueCat billing, and a web build on Vercel from the same codebase. Getting there meant a 14-day closed beta with real testers, a full pre-production audit, and Google's production review.

The offline-first architecture proved itself in the details: one shared rate store powers two tools, network usage dropped to roughly one call per day, and every screen can tell you exactly how much to trust the number it just showed you.

The converter always says which rates it used: live, saved, or built-in, and as of when.
57 survival guide articles, searchable and entirely offline.
Built with
React NativeExpoExpo RouterAsyncStorageRevenueCatEASreact-native-webVercelffmpeg
At a glance
10
Tools, all working offline
57
Offline survival guide articles
~1/day
Network calls after the rate store refactor
$4.99
One-time Pro. No subscription, no ads

Want to see more?

Explore the rest of my work, or get in touch about a project.