NineOne logo

NineOne

Offline-First Is Not Just a Local Database: The Hard Problem Serverpod 4 Still Has to Solve

Serverpod Offline Sync makes a serious CRDT-based attempt at offline-first Flutter apps. The difficult work is still ownership, conflicts, invariants, and proving convergence.

In the previous article, I explored how Serverpod 4 brings Flutter, backend code, a database, and coding agents into one development loop.

That improves how quickly a team can build software.

It does not answer a harder mobile question:

What happens when the network disappears and two devices change the same data?

Offline-first is often reduced to “store everything in SQLite.” That is local caching. It is not synchronization.

Serverpod’s new serverpod_offline_sync package is an ambitious step in the right direction. It adds a delta-CRDT synchronization layer that runs equivalent logic against client SQLite and server PostgreSQL databases. It is also explicitly still under development and not ready for production use, so it should be evaluated as a technical preview rather than assumed infrastructure. The package documentation is unusually candid about both the model and its constraints.

This is where the real engineering work begins.

Offline-first is a synchronization problem

Many applications can read data offline. The difficulty starts when multiple replicas modify it.

Phone A
Customer status = "Paid"

Phone B
Customer status = "Cancelled"

Server
Customer status = ?

Both devices may have been offline. Both users may reconnect later. There is no SQLite API that can decide which value is correct.

That is not a storage problem. It is a distributed-systems problem involving ordering, ownership, conflict resolution, deletes, relationships, and authorization.

Three generations of offline applications

Most mobile products sit somewhere along this spectrum.

1. Cache-first

Server
  ↓
HTTP API
  ↓
SQLite cache

The local database is an optimization. A user cannot make meaningful changes without a network. News readers often work this way: simple and reliable, but not offline-first.

2. Queue-based offline

SQLite
  ↓
Upload queue
  ↓
Server

The app records local actions and replays INSERT, UPDATE, and DELETE requests after reconnecting. This is a valuable improvement, but a queue only retries operations. It does not decide whether the resulting data is correct.

3. A synchronization engine

SQLite
  ↕
Synchronization engine
  ↕
PostgreSQL

The application writes local data. The synchronization layer understands replication, conflict handling, row visibility, deletes, restores, and constraints. That is the direction Serverpod is pursuing.

What Serverpod’s CRDT layer changes

The package is designed to preserve Serverpod’s ordinary development model. A team works with generated models and normal CRUD calls; the synchronization layer tracks operations atomically and replicates them.

According to its documentation, the design includes:

  • delta-CRDT replication with identical merge logic on client and server;
  • one-shot and continuous synchronization;
  • field-level ordering through hybrid logical clocks;
  • row lifecycle controlled by a monotone tombstone model;
  • foreign-key actions and unique constraints preserved after merges;
  • conflicts that require a person surfaced as visible state instead of silently discarding data;
  • scoped, read-only or read-write sharing.

That last point is important. CRDTs offer deterministic convergence. They do not know what the product should do.

If two salespeople rename the same customer to two different values, a deterministic merge can select a value. The correct business outcome may still be “ask a person.” Technology can settle a conflict without making it a good product decision.

Offline-first is really about ownership

Synchronization is dangerous when it is described as “replicate everything.” Serious products need to answer a different question:

Which data is this user allowed to receive and change?

Serverpod models this through scopes. A row belongs to one scope; a user has a personal scope and may have membership in shared scopes with read-only or read-write access. Scope management remains a domain responsibility on the server, where an application can enforce its own invitation, organization, and authorization rules.

This matters for teams, workspaces, private records, and multi-tenant applications. Synchronization without ownership is data leakage with better network behavior.

Constraints are where replication becomes difficult

The expensive synchronization bugs are usually not visible at the moment a user performs an action.

Device A: delete customer
Device B: edit customer
Device C: create invoice for customer

Every device may succeed locally. The hard problem is whether all replicas can later converge without violating referential integrity or silently losing meaningful work.

This is why Serverpod’s documented requirements matter as much as its happy path. Synced tables must exist on both ends, use a UUID primary key, and include a scope relationship. Unique indexes must include the scope; several relation shapes are intentionally limited; and foreign-key relations need to be deferrable. These are not incidental setup details. They are the cost of maintaining a deterministic model across disconnected replicas.

Serverpod and PowerSync make different trade-offs

PowerSync approaches the same product space with a backend-agnostic service that keeps a backend database synchronized with in-app SQLite. It leaves application-specific write handling in the existing backend API.

That is attractive for a heterogeneous organization that already has a server, authorization model, and integrations it wants to retain.

Serverpod’s tighter approach uses its own models, ORM, generated client, migrations, sessions, and scoped data model. For a Flutter team already committed to Serverpod, that can mean less integration work and a more coherent programming model.

Neither approach is universally better. They optimize different constraints:

Need Likely direction
Existing, mixed-language backend Backend-agnostic synchronization layer
New, Dart-first Serverpod product Integrated Serverpod synchronization
Business-specific merge decisions Explicit domain workflow, regardless of engine
High-risk or regulated data A pilot with conservative scope and rigorous review

Agentic development makes sync mistakes more consequential

Serverpod 4 lets an agent update models, create migrations, modify Flutter code, reload the server, and inspect results. Add synchronization to that workflow and an agent may also alter synchronized tables, scopes, or replicated fields.

The dangerous failures are no longer only syntax errors and visible exceptions. They can be distributed data errors that appear after a delayed reconnection—or only when two people edit the same record in different places.

The safest model remains supervised autonomy:

Agent proposes model and migration changes
        ↓
Tests run against disposable replicas
        ↓
Human reviews ownership, conflicts, and data impact
        ↓
Controlled CI promotes approved changes

Offline-first needs a different test discipline

Unit, widget, and integration tests remain necessary. They are not sufficient for synchronization.

A useful scenario test looks more like this:

Phone A goes offline
        ↓
It modifies an order
        ↓
Phone B goes offline
        ↓
It deletes the same order
        ↓
Both reconnect
        ↓
Verify the final state, permissions, and user-visible conflict

Then vary the conditions: duplicate operations, delayed packets, partial sync, clock drift, an interrupted migration, relationship changes, and an authorization change during the session.

The goal is not merely to prove that data transfers. It is to prove that the system converges while preserving the product’s invariants.

The real work starts after adding SQLite

Offline viewing, offline editing, offline collaboration, offline payments, offline inventory, and offline authorization are different problems. Each needs a deliberate answer for source of truth, conflicts, deletes, replay, duplication, and ownership.

Serverpod’s synchronization effort is promising because it tries to move replication mechanics out of everyday application code while retaining relational guarantees. If it matures, Flutter teams could spend much less time rebuilding infrastructure that few products should implement from scratch.

But “the engine handles sync” should never become “the product no longer needs distributed-systems thinking.”

SQLite solves local storage. The difficult question is whether independent copies of reality can converge into a trusted system without violating what the product promises.

Further Reading

Next step

Want to see the build side too?

Browse the project archive to see how these notes translate into product work, or get in touch if you want a similar implementation approach on your team.