One Codebase, Three Platforms — Why Jottii Feels Native Everywhere
One Codebase, Three Platforms — Why Jottii Feels Native Everywhere
Most cross-platform apps make one of two trade-offs. Either they pick a primary platform and treat the others as second-class (a great iOS app and a barely-functional Android app), or they go web-only and pretend the phone experience doesn't matter.
Jottii is built differently. One codebase — Expo + React Native — ships to web, iOS, and Android. The same features, the same data model, the same encryption layer, on every device. This is partly an engineering choice and partly a user-facing one. It changes what you can expect from Jottii regardless of which device you're on.
The decision
When we started Jottii, we had to pick a stack. The options were roughly:
- Native iOS + native Android + web. Three codebases, three teams' worth of work for a small team.
- iOS + Android via React Native, web via a separate codebase. Two codebases, two render layers.
- Expo + React Native Web. One codebase that targets all three.
- Web-only. Cheapest, single codebase, but mobile is a worse experience.
We picked 3. The reasoning was about user experience first and engineering second.
A user who keeps a journal on their phone tonight, opens the laptop tomorrow morning, and checks the same journal on a friend's tablet expects the same app. Same features, same content, same speed. A team like ours can't deliver that with three separate codebases — the smaller features (a calendar quirk, an editor shortcut, a sync edge case) inevitably ship to one platform and lag on the others. With a single codebase, every feature ships everywhere on the same day.
What runs the same
A surprisingly long list:
- The editor. TipTap on mobile, CodeMirror on web, but both share the same Markdown grammar and produce the same canonical content. Bold means the same thing on web and iOS.
- The crypto layer. tweetnacl runs in the JS engine on every platform. The same
secretboxcall encrypts on every device. (See Why We Chose NaCl Over WebCrypto.) - The local database. expo-sqlite on mobile, sql.js or IndexedDB on web. Same schema, same migrations.
- The sync engine. Supabase Realtime client; identical on every platform.
- The state layer. Zustand. Identical.
- Routing. Expo Router. The same
app/folder powers navigation on web and native. - Theming. A single theme module drives every component.
We have a small amount of platform-specific code — keyboard shortcuts on web, biometric prompts on mobile — but the bulk of the logic is shared.
What that means for users
Three concrete benefits:
Feature parity by default. When we ship the calendar improvement, it lands on iOS, Android, and web in the same release. There's no "Android version coming soon" in the changelog.
Bug-fix parity. When we fix a sync edge case, the fix is in one place. It can't be applied to web and accidentally regress on Android because the code is the same code.
Same experience across your devices. The cognitive cost of switching devices drops to near-zero. You don't have to remember "oh, on the web it's in a different menu." It isn't.
Where one codebase fights us
The honest section. Cross-platform isn't free.
Mobile gestures. Web has clicks, mobile has swipes and long-presses. Reconciling them is fiddly. We have specific patterns for this (drawers vs context menus, depending on platform), and we get them wrong sometimes.
The keyboard story. A floating mobile keyboard interacts with a scrollable text editor differently on iOS than on Android than on a web text input. We've spent more weeks on this than we'd like to admit.
Performance ceilings. A pure-native iOS app could in principle do things RN can't (like complex Metal-based animations). For Jottii's use case — text editing, calendar navigation, encryption — we never hit the ceiling. For a different app (a video editor, say), we would.
Web-specific SEO and accessibility. RN Web is a single-page app. Crawlers don't see your app's routes by default. We solve this for marketing pages by prerendering separately (the blog you're reading is one of these) and accepting that the app itself is a SPA. (See vercel.json and the legal-link injection script for how we handle the basic SEO surface for the app shell.)
Native-feel polish. Some animations and haptics are platform-specific. We do them where they help and skip them where they don't.
The deeper rationale
The core engineering insight: most app features are 80% logic and 20% platform glue. With three codebases, every feature pays the 20% three times. With one, you pay it once and the saved time goes into shipping more features.
For a small team, this trade is decisive. We shipped iOS + Android + web with a four-person team. With separate codebases, we'd have shipped iOS + Android with the same team, and web would have been 18 months out.
For users, that translates to: a Jottii that exists on the platform you actually use, today, with the features the team built.
The Expo SDK specifically
We're on Expo SDK 54 + Expo Router 6 at the time of this post. Two specific reasons we picked Expo over bare React Native:
- OTA updates. We can ship bug fixes to mobile users without going through App Store review for every patch. For a privacy-critical app, the ability to fix a bug fast matters.
- Tooling. Expo's build pipeline (EAS) handles signing, uploads, and updates without us needing a dedicated mobile-DevOps person. For a small team, this is the difference between shipping and not.
The cost is some lock-in — we follow Expo's release cadence and SDK constraints. We've found those constraints to be more guard-rails than walls so far.
What stays the same regardless
The values: privacy by default, offline-first, distraction-free, simple. The platform doesn't change those. The cross-platform commitment is in service of them, not the other way around.
Open Jottii on whatever device you're holding. It'll work the same on the next one.
For the broader engineering picture, see Inside Jottii — Cross-Device Sync Without Reading a Word and Why We Chose NaCl Over WebCrypto.