DEV Community

Abhishek Goyal
Abhishek Goyal

Posted on Originally published at abhishekgoyal.me on

Offline-first with no server: why HelperBook is local-only

A lot of my work has been on offline-first apps that still, eventually, talk to a server. At Pulse, I designed the sync contract with the backend and web teams: payloads, upload semantics, failure behaviour. The app worked without signal, but it was always working towards a moment when it would have signal again and reconcile.

HelperBook, the app I’m building now for Indian households to track staff attendance, advances and salary settlements, doesn’t have that moment. There is no signup and no server. Records live on the device, full stop. I want to explain why that’s a design decision and not a missing feature, and what it costs.

The data is the reason not to have a server

HelperBook stores wage data: how many days someone worked, what they were advanced, what they’re owed at month end. That’s exactly the kind of data a household might hesitate to put into an app if it thought the numbers were going somewhere.

Keeping wage data off any server keeps the app outside data-fiduciary scope under India’s DPDP Act, and it gives households a reason to enter real numbers. Those two things are connected. An app that can’t leak your payroll data because it never has it in the first place is a different trust proposition than one that promises to look after your payroll data on a server somewhere.

No sync engine to design

The technical consequence is that there’s no sync contract to design, no conflict resolution, no retry queue, no “what happens if the upload fails halfway through” states. Records live on-device via SQLDelight, inside a shared Kotlin Multiplatform module. Users own their data through export and restore.

That’s a smaller surface area than Pulse’s sync engine, and it’s smaller on purpose. Every state a sync engine has to handle — offline, online, mid-sync, conflicting edits from two devices — is a state HelperBook doesn’t have. The trade-off is direct: no server means no multi-device. If a household member wants to check the ledger from a second phone, they can’t. And if the phone is lost or wiped without an export first, the data is gone. There’s no account to recover it from. That’s a real cost, and it’s one I chose deliberately rather than one I didn’t notice.

Building the honesty into the data model, not the UI

A local-only app has one more responsibility than a server-backed one: the rules have to be right, because there’s no admin panel on the backend to fix a bad month for someone later. So the product rules that matter most live in the data model rather than as UI copy or a setting someone can miss.

Unmarked days default to present, so a worker is never underpaid because someone forgot to tap. Half-days deduct exactly half, with no rounding surprises at month end. And settlement statements are generated bilingually, in English and Hindi, so the employer and the helper can both read every line without one of them having to trust the other’s translation.

As a simplified illustration, the “unmarked day” rule looks something like this:

// Simplified illustration of the rule, not HelperBook's actual code.
fun attendanceValue(mark: DayMark?): Double = when (mark) {
    DayMark.ABSENT -> 0.0
    DayMark.HALF_DAY -> 0.5
    DayMark.PRESENT, null -> 1.0
}
Enter fullscreen mode Exit fullscreen mode

The interesting part isn’t the code, it’s the default: a missing mark resolves to 1.0, not 0.0. In a local-only app, that decision is the whole safety net. There’s no support ticket, no server-side correction, no second system checking the math. Whatever the function returns is what the worker gets paid.

Release engineering without a backend to hide behind

Going local-only doesn’t remove the need for release discipline, it just moves where it matters. HelperBook ships as a signed AAB with R8 rules written for SQLDelight and kotlinx-serialization, so obfuscation doesn’t break the database layer or the serialised export format. Crashlytics is wired in, but the logs carry no names or amounts. Play’s Data Safety declarations describe what the app actually does. And there’s a staged-rollout plan, so a wider release doesn’t reach everyone at once.

None of that is specific to offline-first apps. But when there’s no backend team and no server logs to lean on for debugging, the discipline around what you ship and how you roll it out is the only safety net you have. It’s currently in closed beta on Google Play with 12–20 households, built with Jetpack Compose, min SDK 24, target API 35.

What I’d tell another builder

If you’re weighing whether a product needs a backend at all, the question I’d ask first isn’t “can we build the sync engine,” it’s “what does the user lose if there’s no sync engine.” For HelperBook, the loss is multi-device access and recovery-without-export, and I judged that a household tracking one home’s payroll would rather have neither of those than have their wage data sitting on a server. For a different product, that trade might go the other way.

The second thing I’d say: local-only doesn’t mean less engineering. It means the engineering moves into the data model and the release pipeline instead of into a sync contract. You still have to get the rules right, you still have to obfuscate and log carefully, you still have to plan the rollout in stages. You just don’t get to blame a server for anything.

More on how HelperBook is built is in the case study.

Top comments (0)