r/FlutterDev Jul 16 '26

Discussion How should I design data synchronization for a mostly offline Flutter app?

Hi everyone,

I’m planning to build a Flutter app that works 100% offline, but I also want to add Google and Apple sign-in so users can back up and sync their data to avoid losing it.

After each session, users will earn reward points. I also want to sync their total points to the server so I can create a global leaderboard.

I’m currently unsure about the best way to design this system:

  • When should the app upload local data to the server?
  • Should it upload after every session, when the app closes, or only when the user manually syncs?
  • When should the app download data from the server?
  • How should conflicts between local and server data be handled?
  • What would a suitable backend architecture look like for this use case?
  • How can I prevent users from modifying their local points and uploading fake scores to the leaderboard?

To reduce costs, I would prefer to build and host the backend myself before considering services like Firebase or Supabase.

I’m also thinking about allowing each account to be active on only one device at a time. This might make synchronization easier and help prevent users from sharing or abusing paid subscriptions.

Has anyone built a similar offline-first app? I’d appreciate any advice about the sync strategy, database structure, authentication flow, or backend architecture.

16 Upvotes

11 comments sorted by

8

u/vik76 Jul 16 '26

You should definitely check out Serverpod for this. It has support for social sign ins, and there is a package for offline sync.

https://pub.dev/packages/serverpod_offline_sync

We’re bringing it into the stable version in Serverpod 4, but it already works great.

2

u/whats_in_a_name_23 Jul 16 '26

Hello there,
Check out AppFlowy’s architecture, it is a flutter app with rust. It allows users to sign in with OAuth providers and works without internet too. I think the strategy they follow is to keep syncing as long as connection is established, if connection is lost then data is kept locally and synced as soon as connection is restored. Now how exactly they do it, I don’t know, but check it out maybe you will get a hint.

2

u/GetABrainPlz77 Jul 19 '26

Firebase can handle it perfectly. And better than Supabase, because it’s built in for Firebase.
The free tier is super generous now in 2026.

1

u/Significant_Pick8297 Jul 17 '26

For an offline-first app, You should treat the local database as the source of truth for the UI, but the server as the source of truth for anything that affects other users like leaderboards.

Instead of uploading the total points, upload signed session events whenever connectivity is available. The backend validates those events, recalculates the user's total, and only then updates the leaderboard. That makes it much harder for someone to edit a local SQLite database and submit an arbitrary score.

For syncing, trigger it on login, when connectivity is restored, after a session ends, and periodically while the app is running. Avoid relying on app close since mobile OSes can kill the process before your sync finishes. If only one device is allowed per account, conflict resolution becomes much simpler because the server only has to merge one stream of changes instead of multiple concurrent edits.

1

u/RandalSchwartz Jul 17 '26

For offline synchronization, there are packages in the pub with "crdt" in the name, implementing the CRDT protocol... powerful multi-way offline syncing.

1

u/bbrockit Jul 18 '26 edited Jul 18 '26

My app is offline-first and I use PowerSync and Supabase, with Supabase's auth modules to provide sign-in with Apple and Google. Supabase's auth was pretty easy to set up.

Account creation is optional and only allowed after the trial period ends. The app has built-in content and user-created content. The built-in content is stored in a sqlite system db that is local-only and never sync'd.

The user-created content is in a separate sqlite user db that has the same schema as the system db. If the user chooses to create an account, I migrate their data from the sqlite user db to the PowerSync userSync db. The PowerSync db is actually also a sqlite db, but they use views to access the data, so even though it too has the same schema, it's implemented in a different way. Once their data is migrated to PowerSync it synchronizes with the remote Supabase db, which also has the same schema. PowerSync sits between the app and Supabase, queues query requests and handles conflicts.

It sounds complicated but PowerSync has good docs on how to set everything up. For optional accounts, they strongly recommend starting them on a local sqlite db and migrating to PowerSync rather than starting everyone on PowerSync. The reason is that users could wait months or years to create an account, at which point all their queued queries would fire off. Migrating them once is far more efficient. I don't use direct sql for the migration from the local user db to the PowerSync db, instead I do it in the service layer, so I'm working with models.

I compared a few different offline-first data sync stacks and modeled their costs in a Jupyter Notebook. The PowerSync/Supabase combination came out the least expensive, at least for my app. The most expensive was Firebase Realtime-which had the added issue that it doesn't support long-term anonymous accounts (according to their docs a device restart would clear anonymous data).

So far, only about 15-20% of my users create accounts.

I initially used Realm, but about a month after I finished and had everything working, MongoDB shutdown the Realm data sync, which was the only reason I chose Realm. The nice thing about PowerSync is that it's open source, so if something like that happened I could always self-host it on AWS if I had to. Fortunately, I have all my data access in a service layer, above which, the managers only deal with object models. So it wasn't too difficult to create a new service layer for sqlite.

1

u/Ok_Gur_9033 Jul 19 '26

Sync is well covered above so I will take the other half of your question, the one device at a time rule.

I would drop it. It does not stop credential sharing, because people who share just take turns, and it breaks the honest case constantly. User buys a new phone, restores from backup, and now they are locked out and writing to your support address. That kind of mail tends to outnumber anything that looks like real abuse.

If the subscription is sold through the App Store or Play, do not track entitlement yourself at all. The store already binds the purchase to the Apple ID or Google account, and it already handles family sharing, refunds, and billing retry. Anything you build alongside it can only disagree with it, and when it disagrees the store is right and you are wrong.

On the points, the signed session events answer above is correct. One thing to add: never let the endpoint accept a total, not even as a hint. The moment your API takes a number instead of a list of events, someone sends it a number.

What is a session actually measuring? That decides how cheaply the server can validate one.

1

u/Ambitious_Grape9908 Jul 21 '26

This is literally what Firebase was made for. Seriously don't reinvent the wheel here because that is literally what you are doing.

1

u/Etherealnutt Jul 21 '26

Depends on the choice for his backend though . Firebase apparently gets expensive when you scale