Blog
FlutterPOSOpen Source

How Flutter POS keeps working offline

The queued actions table behind the offline mode in Flutter POS, and the bugs people found after I published it.

I put Flutter POS on GitHub in July 2024. It was meant as a reference project, to show how I structure a Flutter app with Clean Architecture and how to make a POS work offline first. Now it has around 215 stars and 90 forks, and some people use it as the starting point for their own POS apps. It's also the base of Zirel POS, but that's a different post.

Most questions I get about it are about the offline part, so this post is mostly about that, plus the bugs people found after I published it.

Everything is saved on the device first

Every write in the app goes to SQLite first. Adding a product, creating a transaction, updating the profile, all of it. The screens also read from SQLite, so the cashier doesn't wait for the network at all. Firestore is the copy in the cloud.

When the app is offline, every create, update and delete is also saved in a table called queued actions. This is the entity:

class QueuedActionEntity extends Equatable {
  final int? id;
  final String repository;
  final String method;
  final String param;
  final bool isCritical;
  final String? createdAt;
}

So a queued action is basically a note saying "call this method on this repository with this JSON". For a sale it would be TransactionRepositoryImpl, createTransaction, and the whole transaction as a JSON string. It's pretty low-tech, but it has one big advantage. When something doesn't sync, you can open the SQLite file and read exactly what's waiting.

When the connection comes back, the app loads the queue and runs the actions one by one:

for (final queue in queues) {
  // Pass if the internet goes off in the process
  if (!pingService.isConnected) continue;

  final res = await executeQueuedAction(queue);
  result.add(res.isSuccess);
}

executeQueuedAction checks the repository and method name, turns the JSON back into a model, and calls the matching Firestore datasource. If it works, the row is deleted from the queue. If it fails, it stays there and gets tried again on the next sync.

One thing to know if you fork it. A failed action doesn't stop the loop, the next ones still run. Most of the time that's fine. But if your data depends on order, for example a sale that uses a product you created while offline, you might want to stop at the first failure instead of continuing.

Checking the connection

Being connected to wifi doesn't mean the wifi actually has internet. Since October 2025 the check is done by PingService. It pings 8.8.8.8 every second and keeps the recent latencies. If the last few pings all failed or were slower than a set limit, the app treats itself as offline. So a connection that is technically there but too slow to be useful counts as offline too, which for a POS is the safer choice.

Bugs that came from issues

Most of the real problems in Flutter POS were found by other people, not by me.

In August 2024, a few weeks after I published it, someone reported that the ordered products in a transaction always showed 0. They kept digging and found it only happened on a second device. On the phone that made the sale everything looked fine. On another phone logged in to the same account, the same transaction showed up without its products. You never see this kind of bug if you test with one phone only. It's fixed, but if you change anything around transactions, test with two devices on the same account.

In July 2025 a few came in at once. A black screen on a Samsung A33 that turned out to be the database init, and a transaction screen that couldn't show more than 2 ordered items. Those went out in one update. Later in October 2025 I also made the app block sign out while a sync is still running.

Many other issues were about setup, not code. If you want to run it yourself, these are the ones that keep coming up:

  • Firebase Storage rules. Someone pointed out back in 2024 that the README didn't mention them. You need to set them or uploads won't work.
  • Firestore indexes. You only need three, and id should be an array index because it's used for the transaction list. I actually answered this wrong first in the issue and corrected it a few comments later.
  • Google Sign-In on Android. If you get a clientConfiguration error saying serverClientId must be provided, that's the missing piece.

Printing took a while

For a long time Flutter POS couldn't print receipts. In July 2025 someone asked for thermal printer support and I said I'd add it in the next release. It wasn't in the next release. In February 2026 another person asked again, in all caps. I finally added it on March 2, 2026.

When I did, I put the printer code in its own package, unified_esc_pos_printer, and published it on the same day. It talks to USB, Bluetooth classic, BLE and network printers with the same API. It's on version 3.4 now and gets around 2,400 downloads a month, which is a lot more than I expected for a printer package.

Riverpod

In March 2026 I moved the state management from Provider to Riverpod, in two pull requests. The first one replaced the old dependency injection setup in lib/app/di. The second one moved all the ChangeNotifier providers to Riverpod's Notifier with immutable state classes. The app does the same things as before, but the state is easier to test now.

If you want to use it

The code is MIT licensed and runs on Android, iOS, Windows, macOS and Linux. There's an APK in the releases if you only want to try it. The datasources, repositories and use cases have unit tests, and there's a CLAUDE.md with the project conventions, so if you use an AI coding tool on it the code it writes follows the same structure.

If you need a POS for an actual shop, not a codebase to learn from, that's what Zirel POS is for. And if you find a bug in Flutter POS, please open an issue. That's how most of the bugs above were found.