DEV Community

Cover image for Why “just add SQLite” is not an offline-first architecture in .NET MAUI
Lucas Soares Costa for Maui Offline-First

Posted on Fully Autonomous

Why “just add SQLite” is not an offline-first architecture in .NET MAUI

When people first add offline support to a mobile application, the implementation often looks straightforward:

  1. add SQLite;
  2. store business data locally;
  3. call the API when connectivity returns. That solves local persistence. It does not solve synchronization. In business applications where connectivity cannot be assumed, the hardest part is rarely storing a row offline. The difficult part is safely reconciling local and remote state later. That distinction is the foundation of the architecture behind Maui Offline-First Starter Kit. Local-first means the UI does not wait for the server The first design rule is simple: Ordinary user changes are persisted locally first.

Creating, editing, or deleting locally available data should not require a successful API request.
The local database is the operational source for the application UI.
Instead of an online-first path like:
UI
↓
API
↓
server database
↓
UI
the common write path becomes:
UI
↓
local mutation
↓
SQLite
↓
durable Outbox
↓
Sync Engine
↓
API
The network becomes part of synchronization rather than a prerequisite for ordinary CRUD.
Why a durable Outbox matters
If a local change exists only in memory, application termination can lose the fact that it still needs to reach the server.
The reference architecture persists synchronization operations separately from business rows.
A durable operation records information such as:

  • a stable operation identity;
  • entity identity and type;
  • operation type;
  • the server version the change was based on;
  • serialized payload;
  • attempt metadata;
  • dispatch status. The operation ID remains stable across retries, which is essential for idempotency. Successful requests can look like failures Consider this sequence:
  • the client sends a Create operation;
  • the server commits it;
  • the response is lost;
  • the client retries. Without idempotency, the retry can produce a second logical mutation. The server therefore needs to remember processed synchronization operations and return the same logical result when the same operation is replayed. This is one of the places where “sync when the internet comes back” becomes much more than a connectivity check. Push, Pull, and Bootstrap are different jobs Synchronization is not simply: upload local rows, then download remote rows.

The reference implementation separates three flows.
Push sends durable local mutations.
Pull receives incremental server-side changes after a persisted cursor.
Bootstrap establishes the initial synchronized state for a local profile that does not yet have one.
Server changes use a monotonic sequence rather than relying on device clocks, so the client can maintain a durable pull cursor.
Optimistic concurrency makes conflicts explicit
Each synchronized server entity has a version.
Local updates are sent with the server version they were based on.
If the server has already moved forward, the local write cannot silently overwrite newer state.
Instead, the client can persist a conflict containing both sides of the disagreement and let the user or application resolve it deliberately.
In the reference implementation, conflicts retain:

  • the local snapshot;
  • the remote snapshot;
  • the current server version;
  • conflict source;
  • resolution state. The important point is that conflicts are data, not an exception that disappears when a request ends. Deletes need synchronization too Hard-deleting a server row immediately creates another problem: How does a second device discover that the record disappeared? The architecture therefore uses tombstones. A delete becomes a versioned synchronization event. Other clients can observe that event and remove the record from their ordinary local queries while still preserving enough server history for synchronization. Authentication also has an offline dimension The reference application uses access and refresh tokens, but authentication behavior also needs to account for offline startup. A previously authenticated user should still be able to open locally available application data when the API is temporarily unreachable. That means session restoration, secure token storage, refresh behavior, and local profile ownership all matter to the offline-first design. Diagnostics are part of reliability Synchronization problems are difficult to debug if the application only exposes a generic error message. The reference implementation includes:
  • current synchronization state;
  • pending, failed, and conflict counts;
  • durable queue diagnostics;
  • recent synchronization history;
  • last successful synchronization;
  • manual Sync Now;
  • safe error information. A sync engine that works but cannot explain what it is doing is much harder to operate in production. Testing the failure paths The first release currently contains 375 automated tests across the Core, Infrastructure, and API layers. The test suite covers scenarios such as local-first mutations, durable Outbox behavior, Push/Pull/Bootstrap, idempotent retry after a lost response, conflict handling, tombstones, authentication lifecycle, reconnect behavior, and diagnostics. Windows and Android are functionally validated for v1.0.0. The iOS and Mac Catalyst source targets are maintained and compile-checked, but they are not functionally certified in this release. What v1 intentionally does not try to solve The first release does not claim to provide guaranteed background synchronization on every platform, attachment/media synchronization, push notifications, real-time synchronization, or a hosted backend. Those are separate concerns, and including all of them in the first reference architecture would make the core synchronization model harder to understand. The real offline-first problem Offline-first architecture is less about “having SQLite” and more about deciding what should happen when:
  • the network disappears;
  • an operation must survive application restart;
  • requests are retried;
  • responses are lost;
  • two devices edit the same row;
  • a record is deleted elsewhere;
  • a new device needs an initial synchronized state;
  • the app reconnects halfway through a workflow. That is the infrastructure I wanted to make reusable. I packaged the reference implementation into Maui Offline-First Starter Kit 1.0.0. The product includes the .NET MAUI application, ASP.NET Core API, source code, documentation, commercial license, and the full automated test suite. Architecture, demo, screenshots, and licensing details: https://maui.lucassoaresdev.com/?utm_source=devto&utm_medium=article&utm_campaign=launch_v1

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.