June 5, 2026 · 3 min read

Offline-first by design: the architecture behind Balyria

  • offline-first
  • react-native
  • architecture
  • balyria

Balyria is a personal finance app for understanding your money before payday — bills, loans, debts, installments, income, and forecasts. From the first sketch I made one decision that shaped everything else: the app is offline-first, and your financial data lives on your device, not on a server.

Here's why, and how it's put together.

Why offline-first for personal finance

Money apps carry two expectations that pull in opposite directions: they must be instant, and they must be private. People check their balance in the time it takes to glance at a screen — a spinner waiting on a network round-trip is a failure. And financial records are about as sensitive as personal data gets.

Offline-first resolves both. If the source of truth is the device:

  • Every read is local, so the UI is immediate.
  • The app works on a plane, on the subway, anywhere — there's no "you're offline" state because there's no dependency on being online.
  • There's no central database of people's finances to breach, because there isn't one. Privacy becomes an architectural property, not a promise.

The storage split: durable vs. fast

Not all on-device state is the same, so Balyria uses two stores for two jobs.

  • SQLite holds the financial records — accounts, transactions, debts, installment plans. It's relational data with relationships and queries, and it needs to survive app restarts and updates. SQLite is the right tool: durable, queryable, and battle-tested.
  • MMKV handles small, hot key-value state — preferences, flags, lightweight UI state — where the overhead of a SQL round-trip isn't worth it. It's extremely fast and a good fit for the things you read constantly and don't need to query.

The rule of thumb: if it's a record you'd put in a table, it goes in SQLite; if it's a setting you'd read on every render, it goes in MMKV.

State management with Zustand

On top of persistence sits app state, managed with Zustand. I reach for it because it stays out of the way: a store is a hook, updates are plain function calls, and there's no boilerplate ceremony between a user action and the screen updating.

The flow is straightforward — a user action updates the store, the store persists to SQLite/MMKV, and the UI re-renders from the store. Because persistence is local and synchronous-feeling, the screen never waits.

What you give up — and what you don't

Offline-first isn't free. The honest tradeoffs:

  • No cross-device sync out of the box. When the device is the source of truth, syncing to another device is a deliberate feature, not a default. Balyria leans on export/backup tooling instead.
  • You own your backups. Data living on-device means the user is responsible for it. That's a privacy win and a support consideration at the same time.

What you don't give up is monetization or polish. Optional one-time Pro and Power upgrades run through Google Play Billing and RevenueCat — purchase entitlements are the one thing that legitimately needs a network, and they're cleanly separated from the local-only financial data.

The takeaway

Architecture is mostly about deciding where the truth lives. Putting it on the device made Balyria fast and private by construction, and pushed the few things that genuinely need a network (purchases) to the edges where they belong.

You can see the full product in the Balyria case study, or read more of how I build systems.

Need a production-grade backend, integration, or automation system?

Let's turn the workflow into reliable software.