Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

I built Mindleau so people can write journal entries and track moods without first creating an account or connecting to the internet. The app saves entries to a local Drift database immediately; optional Firebase sync runs separately in the background. This is one implementation and its tradeoffs, not a universal architecture prescription or an independently verified security guarantee.

Start with a local source of truth

Mindleau is a free iOS and Android brain-dump journal and mood tracker. Its core requirement was that a journal entry should be usable even when a person has no account or network connection. I chose Flutter for the app, Drift for typed SQLite storage, and Firebase Authentication and Cloud Firestore for optional synchronization. Provider, ChangeNotifier, and ValueNotifier handle app state.

The key boundary is that the interface does not call Firestore. The UI reads and writes through a repository, which persists data in SQLite. A separate sync service handles cloud communication. When a user saves an entry, the repository inserts it locally, marks it as pending, and triggers a sync attempt without making the UI wait for that attempt to finish.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“The central rule: the UI never talks to Firestore.” — Ayesha Iftikhar

This makes the local save path independent of a successful cloud request. It also separates two responsibilities: the repository answers the app’s immediate data needs, while the sync service attempts to bring local and remote data into alignment.

Represent sync state with each row

For rows that sync, I added metadata alongside the journal data. The fields let the app identify a corresponding remote record, distinguish pending work, and retain deletion intent.

  • Remote document ID: identifies the associated Firestore document.
  • Updated timestamp: supports determining which version is newer under the chosen conflict rule.
  • Deleted timestamp: represents a soft deletion, or tombstone, that can be synchronized instead of simply erasing the row locally.
  • Sync status: indicates whether a local change still needs to be sent.
  • Last-sync timestamp: records when the row was last synchronized.

A tombstone matters because deleting a row outright on one device would otherwise give the other device no record that a deletion happened. Keeping deletion state lets the sync process propagate that intent. This is a straightforward per-row approach, not the only possible design: a separate outbox could track changes independently of the application tables.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Run synchronization as background work

The sync service first pulls remote changes, then pushes local preferences and pending records. The save operation does not wait for this sequence, so a slow or unavailable network does not block the local entry from appearing in the app.

  1. Save locally: insert or update the SQLite row and mark it pending.
  2. Attempt sync: trigger the separate sync service without awaiting it in the UI flow.
  3. Pull remote changes: fetch remote updates before pushing local preferences and pending records.
  4. Push pending work: send local changes that have not yet been synchronized.
  5. Retry later when possible: retain pending work if an attempt fails so it can be retried when connectivity returns.

I wrapped network calls in a 15-second timeout to avoid waiting indefinitely on a flaky connection. Sync errors are logged. Since the UI does not depend on the network operation completing, a failed attempt leaves the locally saved entry available and the outstanding work eligible for a later retry.

Choose a conflict rule with known consequences

I used updatedAt last-write-wins conflict resolution. When local and remote versions differ, the newer timestamp determines which version prevails. This keeps the rule relatively simple, but it is not a merge of both edits.

If someone edits the same entry offline on two devices, one edit can be lost when the devices synchronize. I considered that tradeoff acceptable for this personal-use app; that is my judgment about this use case, not a claim that last-write-wins is generally safe. A conflict-preserving merge strategy, such as a CRDT, can avoid some forms of lost work, but introduces additional complexity. The choice depends on how costly conflicting edits would be for the app’s users.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Allow local use without an account

When synchronization begins, the app signs the user in anonymously with Firebase Authentication. The anonymous identity provides a UID for cloud data without requiring account creation before someone can start journaling. The author’s implementation allows an email-link credential to be linked later to the same UID.

If anonymous authentication is disabled or unreachable, the app skips sync and continues using local SQLite. That fallback is central to the local-first behavior: account and network availability affect cloud synchronization, not the ability to keep using the journal locally.

Plan database migrations for existing installs

Adding sync metadata changes the local schema, so I versioned the Drift database and added upgrade steps to introduce the required columns and table. The migration detail that caught my attention was the difference between a Dart-side default and a SQL default.

When adding a NOT NULL column to an existing SQLite table, old rows need a valid value. Drift’s clientDefault runs in Dart; it does not provide the database-side default needed by a raw ALTER TABLE migration. The migration therefore needs an appropriate SQL-level default for existing rows. Treat schema upgrades as part of the shipped app’s behavior: a schema change that works for a fresh install may still fail or leave invalid data on an existing database.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Sync only data with cross-device value

I kept guided stillness sessions local. Those sessions feed streaks and a 30-day heatmap, but I judged their cross-device value too limited to justify the added synchronization work and having that data leave the device.

The broader decision is not simply “sync everything” or “sync nothing.” For each data type, weigh whether cross-device continuity matters enough to justify the additional state, migration, conflict handling, and cloud transfer. Selective sync can keep the architecture and data sharing narrower, but it also means that some app state will not follow a user to another device.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Tradeoffs in this implementation

Decision What this implementation does Tradeoff
Local use or account-first use Allows local SQLite use before authentication; sync begins with anonymous authentication. Writing can start without an account, while cloud sync depends on authentication being available.
Sync metadata or separate outbox Stores remote ID, timestamps, deletion state, sync status, and last-sync time on syncable rows. Row-level state is directly associated with the data; a separate outbox is an alternative that the source does not compare through testing.
Conflict resolution Uses updatedAt last-write-wins. Simpler resolution can discard one of two offline edits to the same entry; a merge approach adds complexity.
Sync every table or select data Keeps guided stillness sessions local while syncing selected data. Reduces the data sent to the cloud, but those sessions do not become cross-device state.

This account describes the author’s implementation and reported reasoning. It does not provide comparative benchmarks, independent verification of security, or evidence that the same conflict and sync choices suit every journaling app.

Source: Ayesha Iftikhar, “How I Built a Local-First Journaling App with Flutter, Drift and Firebase,” DEV Community, posted September 23 and edited September 26, 2025.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.