cat ~/production/offline-first-pharmacy-platform/REPORT.md

Offline-First Pharmacy Platform

A pharmacy management system that keeps selling when the internet drops, then reconciles every till operation exactly once when it returns.

role Tech Lead, Architecture and Deliverytimeline 2025category platform

Problem

A pharmacy cannot stop serving customers because the connection went down, and it cannot tolerate a sale being recorded twice or stock going negative because two tills sold the last box at the same moment. Connectivity in the market this serves is unreliable enough that offline operation is the normal case rather than the exception.

Approach

Built the whole platform on the assumption that a till works disconnected and syncs later. Every operation carries a client-generated identifier so a replayed batch is recognised rather than reapplied. Stock decrements are a single guarded increment rather than a read followed by a write, so overselling is structurally impossible instead of merely unlikely. Offline edits use optimistic concurrency with a field allowlist, so a modified client cannot write a revenue total. A sync plugin makes 27 models syncable without touching 27 files.

Outcome

Deployed and running a working pharmacy day to day, across 164 route handlers and 27 models. Sales, returns, stock, suppliers, expiry tracking and reporting all continue through a connection loss and reconcile correctly afterwards.

164Route handlers
27Domain models · all syncable through one plugin
11.6kApplication code · lines
In daily useStatus · running a working pharmacy
architecture --explain

How it's built

Offline-first with exactly-once reconciliation. Operations are idempotent by client-generated id, stock is guarded at the database rather than in application logic, and conflicts are tagged for review rather than thrown as errors. A shared sync plugin gives every model the same sync semantics.

A single guarded increment for stock, never read-then-write

The decrement filter carries the availability condition, so the database refuses the operation if stock is insufficient. There is no window between checking and writing.

Error handling moves to interpreting a zero-match result rather than reading a friendly precondition first, which is less obvious to read and actually correct under concurrency. Without it, two tills selling the last box both read one remaining and both succeed.

Conflicts are tagged, not raised as failures

A conflict during reconciliation is information, not an error. Tagging it keeps the batch moving and puts the decision in front of a human who understands the business context.

Someone has to review tagged conflicts, which is a workflow rather than a silent resolution, and that is the honest trade for a system handling money.

Field allowlist on offline edits

An offline client is an untrusted client. Without an allowlist, a modified device could write a revenue total or a price directly.

New editable fields have to be added to the allowlist deliberately, which is friction that exists precisely so it cannot be skipped.

Proportional discount reversal on returns, in two phases

Returning one line of a discounted three-line invoice has to reverse a proportional share of the discount. Computing the whole reversal before applying any of it means a partial failure cannot leave the invoice half-adjusted.

More intermediate state than adjusting line by line, in exchange for an operation that either completes or does not.

related --by-stack

Adjacent systems

REL-002 / platform

E-commerce and ERP Backend

GraphQL commerce backend where the client rearranges their own storefront from the dashboard, over an inventory ledger with a single mutation point.

REL-003 / platform

Hardware Retail Platform with ERP Sync

Storefront, app and operations dashboard sitting in front of an external ERP, with data flowing both directions and staying consistent.