Consumer & wellness apps

Offline-first app architecture: backups that merge, deletes that stick, DST-safe streaks

Users switch phones, restore old backups and log at 1 a.m. on DST weekends. Here is the offline-first design we built for each moment, and where it still falls short.

On this page
  1. Where offline-first apps quietly lose people’s data
  2. Offline sync conflict resolution: why the usual fixes fail
  3. Offline-first app architecture starts with one document
  4. Merging backups from several devices with tombstones
  5. Habit streaks that survive daylight saving time, late nights and goal edits
  6. Premium that works offline without being given away
  7. Tradeoffs we would harden next, and an offline-first checklist
  8. Questions we hear

The short version

  • With no sync server, make the saved state and the backup file one versioned document, so merging a backup on another device is the sync.
  • Merge backups item by item with newest-edit-wins, and keep deletes as dated tombstones (90 days for us) so old backups cannot revive them.
  • Treat every backup file as untrusted input: validate ranges, migrate old schemas, refuse newer ones.
  • Store days as local calendar keys, not timestamps, and version goal edits, so streaks survive daylight saving time and changed targets.
  • Let paid features work offline for a fixed grace period (7 days for our web license keys), then switch them off until a check succeeds.

Where offline-first apps quietly lose people’s data

It is 12:40 a.m. on the Sunday the clocks spring forward, and a user ticks off the last task of day 23 on her new phone. By breakfast her streak reads zero, a challenge she deleted in January is back, and last week’s notes from her laptop are gone. No server failed, but you may have just lost a paying user: each bug is an offline-first app architecture decision someone made by accident in week one.

We built a local-first habit app for iOS, Android and the web, with no server holding anyone’s habit data. Its job was to put a specific guard on each of those failures.

3.6x

more likely to finish a Duolingo course for learners who reach a 7-day streak (a correlation).

Duolingo Blog, 2022
59%

of 32 popular mental health apps carried a “*Privacy Not Included” warning label.

Mozilla Foundation, 2023
Sep 2025

Atlas Device Sync, the sync service behind Realm, reached end-of-life.

MongoDB documentation

Streaks track closely with finishing, mental health apps have a poor privacy record, and sync vendors can shut down. We designed around five moments where habit apps lose data or trust:

  • Switching phones and restoring a backup that overwrites newer work.
  • Deleting something that an older backup brings back.
  • Logging late at night, or on a 23-hour daylight saving time (DST) day, and losing the streak.
  • Changing a goal, which rescores every past day.
  • Going offline on a paid plan, which either locks out a paying user or gives premium away.

Offline sync conflict resolution: why the usual fixes fail

Each common fix solves one symptom and creates another. “Newest file wins” is the classic: the laptop’s backup is a day newer, so it replaces the phone’s whole file, and every check-in made only on the phone disappears.

Common shortcuts vs what we built
Common shortcut
  • The newest whole file wins
  • A delete removes the row
  • Days stored as UTC timestamps
  • Backups trusted because we wrote them
  • License checked only when online
What we built
  • The newest edit wins, per item
  • A delete leaves a dated tombstone
  • Days stored as local date keys
  • Every import validated like user input
  • Web license: seven days offline, then premium pauses

Renting a sync engine moves the risk rather than removing it. When MongoDB retired Atlas Device Sync, the sync service behind the Realm database, every app on it needed a Realm sync alternative or no sync at all.

Last write wins vs CRDTs for a personal app

Last write wins means that when two copies disagree, the more recent edit replaces the older one. Conflict-free replicated data types (CRDTs) merge concurrent edits automatically so every copy ends up the same, which earns its cost in shared documents with many writers.

A habit log has one owner, usually on one device at a time, so last write wins per item, plus tombstones, covers most real cases with far less machinery.

Offline-first app architecture starts with one document

There is no sync server. The app’s saved state and its backup file are the same document, so merging a backup on another device is the sync, and there is no second format to drift.

That document holds a version number (schemaVersion), challenges keyed by id, day logs keyed by challenge and date, settings, the device id and tombstones (dated delete records, explained below). Every change stamps a last-edited time (updatedAt) on the item it touches.

Optional cloud backup puts the same file in a hidden, app-only folder in the user’s own Google Drive, reached through Google’s standard sign-in (OAuth with PKCE). It is built but stays off until its Google setup is done, and a Drive restore takes the same path as a file picked by hand.

What happens to every imported backup
  1. Parse

    Invalid JSON is refused with a plain message, not a crash.

  2. Check types and ranges

    Every field is type-checked, and fixed choices, numbers and dates must hold allowed values before anything touches the store.

  3. Migrate forward

    One pure step runs per schema version, and files from a newer app are refused.

  4. Confirm counts

    The user sees what the file holds, then chooses merge or replace.

  5. Merge per item

    Items combine by id under newest-edit-wins and tombstones.

The hand-written validator checks that values make sense, not only that they have the right type: durations of 1 to 365 days, count targets of 1 to 9,999, moods of 1 to 5, at most 8 live tasks. Date keys must survive a round trip through the calendar, so “2026-02-30” is rejected.

Merging backups from several devices with tombstones

A tombstone is a dated record that says “this challenge was deleted on March 3 at 2:02 p.m.” It exists so an old copy cannot bring the challenge back. On merge, the rules apply in this order:

  1. Tombstones older than 90 days are dropped first, so the list cannot grow forever.
  2. For each challenge and each day log, the copy with the newer updatedAt wins.
  3. A tombstone newer than the surviving copy keeps the challenge deleted.
  4. An edit made after the delete wins, so a challenge still in use on another device returns on purpose.
  5. Day logs whose challenge did not survive are dropped.
  6. Settings stay local on merge. Replace takes the backup’s settings but fills missing parts with defaults.

A delete is a fact with a date, not a missing row.

Ninety days is a sizing decision: it must outlast any realistic gap between a user’s backups. Tests pin the edges: delete then re-import stays deleted, an edit after the delete revives the challenge, and a tombstone 91 days old is gone.

Habit streaks that survive daylight saving time, late nights and goal edits

A day in a habit app is a date on the user’s calendar, not 86,400,000 milliseconds. In most places that observe DST, each year has a 23-hour day and a 25-hour day, and fixed-length math drifts on both.

So every day is stored as a local key such as 2026-03-08, built from the phone’s own year, month and day. Gaps are computed from those calendar parts with Date.UTC, so DST weeks still produce whole numbers.

  • A day can start at 2 a.m. Users can move the start of their day to any hour up to 4 a.m. (it is midnight by default), so a 1:30 a.m. check-in counts for the night before.
  • Today refreshes itself one second after the next day starts and on every return to the app.
  • An unfinished today never breaks yesterday’s streak. Paused days are skipped, so they neither count nor break a run.
  • Goal edits are versioned. Changing a task’s type, target or unit retires the old rule from today and adds a new one.

Raise a water target from 6 glasses to 8 on day 10, and days 1 to 9 still score against 6. It is the same principle as snapshotting a record when it is issued, which we used for prescriptions a pharmacist can verify.

Premium that works offline without being given away

Paying users lose signal on planes and in gyms, and they should keep what they paid for. But a status that never expires without a check invites abuse: switch on airplane mode and keep premium forever.

On iOS and Android, premium follows the store subscription status, and offline the app keeps the last status the store reported. On the web, premium comes from a license key: the app re-checks it daily and trusts it for at most a week without a successful check.

When the week runs out, premium fails closed: it switches off until a check succeeds, instead of staying on by default.

How the web app decides whether premium is on
  1. Read stored status

    A revoked or expired license means free, whatever else is true.

  2. Check the clock

    A last check dated in the future means the clock moved back, so it counts for nothing.

  3. Apply the grace

    A good check within 7 days keeps premium on.

  4. Revalidate daily

    On launch and on return, a check older than 24 hours is repeated.

  5. Handle the reply

    A network error changes nothing, and a rejected key is revoked at once.

Billing is split by platform to respect store rules: the web uses a merchant of record (a reseller that handles tax and payment) and license keys, while iOS and Android use store billing with no external payment link. Both paths write one premium on/off switch.

Tradeoffs we would harden next, and an offline-first checklist

Back to 12:40 a.m.: with her day start moved to 2 a.m., the tick counts for Saturday. The January tombstone keeps the challenge deleted, and her laptop’s backup merges in item by item. The design still has limits, and some are next on our list:

  • Drive holds one file. Back up now overwrites it, so a second device should restore and merge first. Merging before upload is next.
  • Photos stay on the device. Backups carry check-ins, notes and moods, not photos, and Replace clears local photos.
  • Device clocks pick the winner. A phone set a day ahead wins edits it should lose. A hybrid logical clock (a timestamp that also counts edits already seen) would at least make an edit after an import always win.
  • Ninety days is a bet. Merging an older backup can return long-deleted items. We would warn when a file predates the window and ask before restoring items missing here and older than it.
  • Client-side checks can be bent. The clock guard catches a clock set before the last check, not one held inside the window. A signed entitlement and a last-seen time that only moves forward come next.
  • One JSON write per change. Our plan puts the limit near 20,000 day logs, after which logs would move to SQLite behind the same export format.
  • DST tests need a DST zone. CI defaults to UTC, where clocks never change, so we would pin several TZ values.
Offline-first readiness checklist
  • Saved state and backup file share one versioned schema.
  • Every item has a stable id and an updatedAt stamped on each change.
  • Deletes write a dated tombstone with a chosen lifetime.
  • Old files migrate forward, newer ones are refused, and each version’s validator checks ranges, not only types.
  • Import never changes premium status, the app lock or the device id.
  • Days are local YYYY-MM-DD keys, and toISOString() never touches day logic.
  • Editing a goal retires the old rule instead of rewriting history.
  • Paid features revalidate daily and switch off after a fixed offline grace.
Download this checklist (PDF)

If reviews mention lost streaks or deleted items coming back, or you are replacing a sync vendor, book a free 30-minute discovery call. Bring your schema and your worst support ticket, and we will tell you which gaps to close first. We design and build custom mobile and web software like this.

Questions we hear

What is offline-first app architecture?

Offline-first app architecture means the app reads and writes to on-device storage first and treats the network as optional. Features work without a signal, and syncing or backup happens later, merging changes safely. Local-first goes further: the device holds the primary copy, and a server copy, if any, is a replica rather than the source of truth.

Is last write wins good enough, or do I need CRDTs?

For personal data with one writer at a time, last write wins per item plus tombstones is usually enough and far simpler. CRDTs pay off when several people edit the same document at once, such as shared notes. The catch: the losing edit is discarded, and a device with a wrong clock can win edits it should lose.

How long should tombstones be kept in an offline-first app?

Longer than the longest realistic gap between a device’s backups or syncs. We chose 90 days for a habit app. Shorter windows let old backups resurrect deleted items, while longer ones grow the file. When an import is older than the window, warn the user and ask before restoring items that are missing locally and older than the window.

How do you calculate streaks across time zones and daylight saving time?

Store each day as a local calendar date such as 2026-03-08, never a UTC timestamp, and compute gaps from the year, month and day parts. Then 23-hour and 25-hour days still count as one day. Because keys follow the phone’s clock, a traveler’s day follows local time. A flight west across the date line can skip a whole date, so decide up front whether that counts as a miss.

What can replace Realm’s Atlas Device Sync?

Either another sync engine, or no live sync at all if each user owns their data. Shared data needs a sync engine and carries its vendor risk. Personal data can use a versioned backup file that merges item by item, as our habit app does. It is not a drop-in swap: devices converge only when a user imports a backup.

Sources

  1. The habit-building research behind your Duolingo streakDuolingo
  2. Shady Mental Health Apps Inch Toward Privacy and Security Improvements, But Many Still Siphon Personal DataMozilla Foundation
  3. Atlas Device SDKs DeprecationMongoDB
Written by the SeriusTech engineering team

We build custom software, business systems, applied AI and localization technology for growing businesses. Every technical claim in this note was checked against the code it describes. Products and clients stay unnamed.

Custom software servicesFollow for new field notes
Free 30-minute discovery call

Want this solved in your business?

  1. Send a short request. We reply within 1 business day with times that suit your time zone.
  2. Talk it through for 30 minutes, free and with no obligation.
  3. Leave with a suggested first step, such as a fixed-price discovery sprint.
Free, 30 minutes, no obligation

Request your discovery call

Tell us a little about your project. We reply within 1 business day with times that suit your time zone.

Open email draft

This opens your email app with a draft to contact@seriustech.com. Nothing is sent until you send it.