Building a Mobile App With Offline-First Sync: Real Cost, Real Edge Cases
Offline-first sync is buildable, and Digital Heroes has shipped it across field service, retail audit, logistics and clinical apps. Honest bands from our delivery data: adding it to an existing app runs $15,000 to $60,000 in 4 to 8 weeks. As part of a first release it runs $50,000 to $130,000 over 10 to 16 weeks. A full multi-tenant platform with offline as a core guarantee runs $150,000 to $350,000 over 6 to 12 months. The number moves on one question: how many users can edit the same record while offline. One editor per record is cheap. Many editors per record is where the budget doubles.
What offline-first actually means once real users touch it
Most vendors hear "works offline" and quote a cache. They store the last API response in local storage, show it when the network drops, and call it done. That is a read cache, not offline-first. Offline-first means the local database is the source of truth for the UI, every write lands locally first and returns instantly, and a background process reconciles those writes with the server whenever a connection appears. The user never waits on the network. That single inversion changes your data model, your API contract, your ID strategy and your support process.
The naive version fails like this. A field technician in a basement closes out a job at 10:40am, marks 3 parts consumed, uploads 4 photos, and taps Complete. No signal. At 11:15 the dispatcher, seeing the job still open in the web console, reassigns it to a second tech and edits the parts list. At 11:50 the first tech surfaces in the parking lot and the phone syncs. With naive last-write-wins on the whole record, the tech's payload has an older client timestamp than the dispatcher's edit, so the entire completion is silently discarded: photos gone, parts gone, job still open. Or the clock on that Android device is 6 minutes fast, the tech's stale write wins instead, and the dispatcher's reassignment vanishes. Both outcomes are data loss. Neither throws an error. Nobody finds out until the customer is invoiced for the wrong parts three weeks later, and by then your logs have rotated. That is the 3am page you cannot debug, because the evidence was overwritten by design.
The second classic failure is duplicates. The tech taps Complete, the request actually reaches the server, the server writes the row, and then the connection dies before the 201 comes back. The client retries. Now there are two jobs, two invoices, two parts deductions. Every offline app produces this on day one unless you designed against it.
Conflict resolution is a product decision, not a library setting
There is no framework that solves conflicts for you. You choose a strategy per entity, and someone in the business has to sign off on the choice.
Last-write-wins is the cheapest and it is defensible for genuinely single-owner data: a user's own profile, a draft note only they touch. If you use it, never trust the device clock. Android and iOS clocks drift, users change them manually, and timezone handling makes it worse. Use a Hybrid Logical Clock or a server-assigned monotonic sequence so ordering survives a wrong device clock. Field-level LWW rather than record-level LWW is a small change with a big payoff: the tech's status field and the dispatcher's assignee field both survive because they are different columns.
CRDTs are the real answer when multiple users edit the same object concurrently and you want automatic merge with no user prompt. Automerge and Yjs are the two production-grade options. Yjs is faster and has the better ecosystem for text and list editing. Automerge has cleaner JSON document semantics and built-in history. Both cost you storage: CRDT metadata grows with edit history, and on a document edited daily for a year you can see the doc outgrow the payload by several times unless you compact and snapshot. CRDTs also cannot express business rules. A counter CRDT will happily merge two concurrent decrements of your inventory to minus 3. If your data has invariants, CRDTs merge the data and break the meaning.
Operational transform is where you end up for collaborative rich text if CRDT overhead is unacceptable, and it is significantly harder to get right. Do not build one.
The strategy most business apps actually need is server-authoritative reconciliation: the client sends intents, not rows. Not "job.status = complete" but "CompleteJob(jobId, at, partsUsed)". The server replays the intent against current state, applies business rules, and can reject with a reason the app can show. This is more work up front and it is the only approach that survives an audit, because you keep both versions and the decision.
Whatever you pick, budget for the conflicts you cannot auto-resolve. You need a UI for them, a queue for the ones that need a human, and a policy for who wins. Teams that skip this ship an app that silently eats data for six months.
IDs, idempotency, and the duplicate record problem
If the server assigns IDs, offline creation is impossible without a temporary local ID, and then every foreign key pointing at that temp ID has to be rewritten when the real ID arrives. That rewrite is a recursive graph update and it is a common source of orphaned children. The fix is to generate IDs on the client: UUIDv4 works, UUIDv7 is better because it is time-ordered and does not shred your B-tree index the way v4 does on a large Postgres table. ULID is an equivalent choice. The client owns the ID from creation, nothing needs rewriting, and the record has the same identity everywhere from the moment it exists.
Client-generated IDs also give you idempotency for free if you use them properly. Every mutation carries a client-generated operation ID. The server stores processed operation IDs and returns the original result on a repeat rather than executing again. Keep that table for at least 7 days, because a phone can be in a drawer over a long weekend and still retry on Tuesday. Stripe's API is the reference implementation to copy here: idempotency key on the request, cached response, explicit conflict if you reuse a key with a different body.
Then sequence the queue. Mutations are not independent. Creating a job and then adding a note to it must replay in order, and if the create fails permanently the note must not be retried forever. You need a durable outbox with ordering, dependency awareness, retry with exponential backoff and jitter, and a poison-message path after N attempts that surfaces to the user instead of looping silently and burning battery.
The sync protocol and the parts nobody quotes for
Full-table pull on every app open is fine at 500 rows and fatal at 50,000. You need delta sync: the client sends a cursor, the server returns everything changed since. Two ways to build the cursor. A monotonic server-side sequence number or logical clock is correct. A plain updated_at timestamp is the tempting one and it is subtly broken: two rows written inside the same transaction can share a timestamp, and a row committed after your cursor but with an earlier timestamp is invisible forever. If you use timestamps, you need an overlap window and dedupe on the client, and you need to accept that clock skew across replicas will bite you.
Deletes are the thing everyone forgets. If you hard-delete on the server, the deleted row simply never appears in the delta and stays on the device forever. You need tombstones, a retention window for them, typically 30 to 90 days, and a hard rule that any client whose cursor is older than that window must do a full resync instead of a delta. Also plan for a device that has been offline for 4 months and comes back with a cursor older than every tombstone you kept.
Partial sync is what makes it viable at scale. Do not sync the whole tenant to a phone. Sync the user's territory, their next 14 days, their assigned accounts. Then handle the reassignment case: when the scope changes, the rows that left scope must be evicted locally, and the app has to tell the difference between "deleted" and "no longer yours". Getting that wrong leaks another user's data onto a device.
On storage, WatermelonDB and SQLite via op-sqlite or expo-sqlite are the React Native workhorses. PowerSync and ElectricSQL both sync Postgres to local SQLite and are the strongest current managed choices if your backend is already Postgres; Ditto is the option when devices must sync peer to peer with no server in the middle. Room with WorkManager on Android, Core Data with NSPersistentCloudKitContainer or a custom queue on iOS. One correction worth making explicitly, because half the tutorials online are stale: MongoDB retired Realm Sync and Atlas Device Sync on 30 September 2025, so it is not a candidate, whatever a 2023 blog post says.
Two platform behaviours will cost you a sprint each. iOS suspends your app aggressively and background execution is not a promise, so a sync you scheduled may run hours later or not at all until the user reopens the app; BGProcessingTask helps and does not guarantee. Android Doze and per-OEM battery killers, Xiaomi and Huawei are the worst offenders, will kill your WorkManager jobs entirely on some devices unless the user whitelists you. Your sync must be correct when it runs 9 hours late.
Testing, and why offline bugs escape QA
Offline bugs do not reproduce on a desk with fast wifi. The state space is combinatorial: two devices, three entities, network dropping at four different points in the request lifecycle. You cannot manual-test your way through it.
What a competent build does: a deterministic simulation harness that replays scripted operation logs against the merge function, so every conflict scenario is a unit test that runs in milliseconds. Property-based testing with fast-check or Hypothesis to assert convergence, that any two devices applying the same set of operations in any order end in the same state. Explicit tests for the killer case of a request that succeeds server-side and fails client-side. Chaos testing with Charles Proxy or the Network Link Conditioner profiles, not just airplane mode, because 2G with 40% packet loss breaks differently from no network. And schema migration tests against a real device database from the previous version, because a phone that skipped 6 releases will open your app with a 6-versions-old local schema and you get one shot at migrating it without wiping their unsynced work.
Budget 30 to 40 percent of the engineering effort here. On our projects that is not gold-plating, it is the difference between a launch and a rollback.
What this costs and what drives the number
These are Digital Heroes delivery bands from our own project history across 2,000+ builds, not market averages.
Adding offline-first sync to an existing, healthy app: $15,000 to $60,000, roughly 4 to 8 weeks. That is the band when the entity set is small, ownership is mostly single-user, and the backend already exposes clean write endpoints you can make idempotent.
Offline sync as part of a first release: $50,000 to $130,000 over 10 to 16 weeks. Here you are designing the schema, the sync protocol and the app at the same time, which is actually cheaper per unit of sync than retrofitting, because you are not fighting a data model that assumed the server always answers.
A full platform where offline is the product guarantee, multi-tenant, multi-role, audit trail, admin console: $150,000 to $350,000 over 6 to 12 months.
What pushes this specific capability up, in order of impact:
Concurrent editors per record. One owner per record is a small multiplier. Many editors on the same record forces CRDTs or a full intent-based reconciler and roughly doubles the sync engineering.
Number of synced entities and the depth of relationships between them. 5 flat entities is a different project from 30 entities with cascading foreign keys, because every relationship is a referential-integrity question at merge time.
Attachments. Photos, signatures and video are not rows. They need a separate resumable upload queue, chunking, dedupe by content hash, local eviction policy when the device fills up, and a story for the 400MB of unsynced photos on a phone that just broke. Attachments typically add $10,000 to $25,000 on their own.
Retrofitting onto a backend with server-generated sequential IDs and no idempotency. Expect to add 3 to 5 weeks before you write a single line of mobile sync code.
Regulated data. If the offline store holds PHI or payment data you now need encryption at rest with SQLCipher or platform Keychain-backed keys, remote wipe, and an audit log that survives conflict resolution. That is a compliance workstream, not a feature.
Number of platforms. iOS plus Android plus web means three background execution models and, if you share a sync core, likely a Rust or TypeScript core with bindings, which is the right call and adds setup cost.
When you should not build this
If your app is fundamentally a reader with light writes, a form submission, a like, a comment, do not build a sync engine. Build an offline read cache plus a durable mutation outbox with idempotency keys. TanStack Query with a persister, or Apollo Client with its cache persistence, gets you there in 1 to 2 weeks instead of 8. Ninety percent of apps that ask for offline-first need exactly this and nothing more. Be honest about which you are.
If you are pre-product-market-fit and your data model will change 4 more times, take a managed sync service: PowerSync, ElectricSQL, Ditto for peer-to-peer, or Supabase with a local SQLite mirror. You are buying a solved conflict engine and a battle-tested protocol for a monthly bill instead of a quarter of engineering. The cost is real lock-in and a ceiling on custom merge logic, and you should read the pricing page carefully because these products meter on synced operations or data volume and the bill scales with usage in a way a build does not. Take the managed service anyway. You can replace it in year two when you know what your data actually looks like, and you will have shipped.
Build custom when the merge logic is your competitive advantage or a regulatory requirement, when you have hard constraints a hosted service cannot meet such as on-premise or air-gapped deployment, or when you are past 50,000 devices and the metered bill has crossed the cost of a team. Those are the honest reasons. "We want control" is not one.
How to brief and vet a developer for this
Brief them with the answers, not the request. Tell them: which entities sync, how many rows a typical device holds, who can edit each entity concurrently, the longest realistic offline window, whether attachments are in scope, and what the business wants to happen when two people edit the same field. If you cannot answer the last one, that is the first meeting, not a detail for later.
Then ask these. The answers separate people who have shipped this from people who have read about it.
"What is your conflict resolution strategy per entity, and why is it different for each one?" A single global answer means they have not thought about it. Anyone who says "we use last-write-wins" without asking what the data is has already lost your data.
"Where do IDs come from?" If the answer is the server, ask how offline creation works, then watch them build the temp-ID rewrite story live.
"A write reaches the server, the server commits, the response is lost, the client retries. What happens?" If the word idempotency does not appear in 10 seconds, stop.
"How does a device learn that a record was deleted?" Tombstones and a retention window, or they have not shipped this.
"A device has been offline for 90 days. Walk me through the first sync." Correct answer involves detecting a cursor older than the tombstone window and forcing a full resync, and preserving the queued local writes through it.
"How do you test a merge conflict?" Deterministic replay and property-based convergence tests, or the honest answer that they test manually, which tells you what you need to know.
"What happens when the local schema is 6 versions behind and there are unsynced writes in the outbox?" This is the question that finds people who have actually maintained an offline app for two years rather than launched one.
"What does the user see when we cannot merge automatically?" There must be a UI answer. If conflicts are invisible in their design, conflicts are data loss in their build.
Ask them what they would use off the shelf. A senior engineer will tell you which parts of your requirement PowerSync or ElectricSQL already solves and argue you out of half the scope. Someone who wants to hand-roll all of it on a 10-week timeline is selling you hours.
The evidence behind this guide
Independent findings on why this investment pays off. Every link goes to the primary source.
- Standish's 2015 CHAOS research found roughly a third of software projects (about 36% by the Modern definition) fully succeed on time, on budget, and on scope, with top success drivers including executive support, user involvement, and clear requirements/business objectives. Source: Standish Group (CHAOS Report) (2015) →
- US mcommerce reached $280.4 billion in Jan - July 2024 (up 10.2% YoY), accounting for 49.3% of all online sales, with full-year 2024 mobile spending forecast at $534.88 billion. Source: EMARKETER (Insider Intelligence) (2024) →
- Only about 30% of digital transformations succeed at meeting their objectives, but getting six critical success factors in place (leadership commitment, talent, agile culture, progress monitoring, clear strategy, and a modernized platform) raises the odds of success from 30% to 80%. Source: Boston Consulting Group (BCG) (2020) →
- 48% of private companies cite integration with legacy systems or technical debt as a top obstacle to realizing the full value of their digital and AI investments (behind data quality/availability at 72% and gaps in AI fluency or technology talent/leadership at 53%). Source: Deloitte (2026) →
Rohan advises mid-market and enterprise teams on ERP, CRM and custom software, and has led delivery on dozens of business-software builds.
Writes for Digital Heroes, shipping business software for 2,000+ brands across 55+ countries since 2017.