Skip to content
← All work

Retail sales, inventory & stock platform

Asturii

Five codebases behind one shop counter — an offline-first POS that keeps selling when the network does not.

  • Flutter
  • Riverpod
  • Drift
  • Express
  • PostgreSQL
  • React
Codebases
5
Lines of Dart
23.6k
Network required
0

The problem

My mother runs a fashion retail business. The software she had did the thing retail software usually does: it worked until the internet did not, and then it stopped being able to sell anything. A shop cannot pause because an ISP is having an afternoon.

So the requirement was not "build a POS". It was: the counter keeps working, always, and the books reconcile once the network comes back.

Five codebases, one system

Asturii is not one application. It is five, each doing a job the others should not:

  • Mobile app — Flutter, the thing actually used at the counter
  • API — Express and Sequelize over PostgreSQL, the single source of truth
  • Admin console — React, for catalogue and reporting work that wants a large screen
  • Batch image pipeline — Python, generating product imagery at volume
  • Barcode service — label generation for stock

The split is deliberate. The counter app has a hard offline requirement; the admin console never does. Building them as one thing would have forced the harder constraint onto code that did not need it.

Offline-first, properly

"Offline support" usually means a read cache. That is the easy half. The hard half is writes — a sale made with no network is money that must not be lost, duplicated, or silently reordered.

The design that holds:

  • A durable write outbox. Every mutation is written locally first and queued. The sale is complete from the counter's point of view the moment it is recorded, not when the server hears about it.
  • Connectivity-aware draining. The queue drains when a connection genuinely exists, not when the OS claims an interface is up.
  • Retry with backoff. A failing server is retried patiently rather than hammered.
  • Server-wins conflict resolution. Chosen deliberately: with one counter and one back office, a simple rule everyone understands beats a clever merge nobody can predict. If this grows to several simultaneous tills, this is the first decision I would revisit.
  • A visible sync indicator, because the person at the counter needs to know whether the shop is currently ahead of the server.

Local storage is Drift over SQLite — a real relational store, because the outbox needs transactions and the catalogue needs joins.

At the counter

The interface is built around a barcode scanner rather than a keyboard. Continuous camera scanning, hardware and keyboard-wedge scanner support, manual entry as a fallback, and auto-increment when the same item is scanned twice — because in practice that is what scanning the same shirt twice means.

Around that: discounts, tax, customer selection, dues, and checkout that completes offline. Stock is received by scanning the same barcode against an incoming quantity, so the receiving flow uses the same muscle memory as selling.

Reporting exports to PDF and CSV. Payments go through Razorpay.

Where it stands

Partly in production, in a real shop, with real stock. The parts that are live are the ones that matter most and were hardest to get right — catalogue, scanning, sales and sync.

What running it taught me is that offline-first is a product decision before it is a technical one. Almost every hard question was about what the shop should do in an ambiguous state, not about what the code could do.