Offline-first is a data-model problem, not a sync button
Putting a "Sync Now" button in the corner of your interface is an admission that your underlying architecture treats local work as an aberration rather than the primary state. Real offline resilience requires re-engineering how state is born.
Most SaaS applications are designed around optimistic online assumptions: the client displays a spinner, sends an HTTP POST to a remote REST endpoint, waits 150 milliseconds for the database to return an autoincrement ID, and updates the UI.
When you try to retrofit "offline support" onto this architecture, teams invariably install an HTTP caching layer and add a "Sync Changes" button. The moment a store runs offline for eight hours, this approach collapses: autoincrement IDs collide, deleted records re-emerge like ghosts, foreign keys break, and employees are presented with baffling modal dialogs asking them which version of a receipt to discard.
1. The Append-Only Mutation Queue
In NoreOS, state is never modified in-place over the network. Instead, every user action is modeled as an immutable mutation event written immediately to a local disk-backed log (SQLite / IndexedDB on the edge):
interface MutationEvent {
id: string; // Client-generated UUIDv4
tenantId: string;
terminalId: string;
type: 'SALE_COMPLETED' | 'ITEM_VOIDED' | 'STOCK_ADJUSTED';
payload: Record<string, unknown>;
lamportClock: number; // Monotonic counter per register
occurredAt: string; // ISO-8601 wall clock
status: 'PENDING_SYNC' | 'COMMITTED' | 'CONFLICT';
} Because mutations are append-only, the register can commit checkouts with sub-millisecond local latency. Thermal receipt printers fire immediately over USB or Bluetooth ESC/POS protocol. As far as the customer and cashier are concerned, the transaction is finished and permanent.
2. The Problem with Deletes: Tombstones Are Mandatory
In a naive database, deleting a row means calling DELETE FROM items WHERE id = ?. In an offline-first system, an in-place delete is fatal.
Consider this sequence: Register A is offline. Register B deletes an obsolete item. Register A reconnects and syncs its state. If the record on Register B was simply removed, Register A sees no record of the deletion, assumes Register B missed the item, and re-uploads it back to the cloud. The item has risen from the dead.
To prevent resurrection, all deletions must be modeled as explicit Tombstone records:
ALTER TABLE catalog_items
ADD COLUMN deleted_at TIMESTAMPTZ DEFAULT NULL,
ADD COLUMN tombstone_id UUID DEFAULT NULL; When syncing, tombstones propagate forward with identical priority as inserts. Only when all registered edge devices have reported back past the tombstone's Lamport timestamp can background garbage collection safely purge the record.
3. Schema-Per-Tenant as a Reliability Guarantee
In NoreOS, every enterprise tenant lives in an isolated PostgreSQL schema. This architectural decision was driven by offline synchronization safety:
- No Blast Radius: A corrupted sync packet or migration mismatch on one retail chain's terminal can never lock or crash tables used by other merchants.
- Independent Migrations: Stores in remote areas that stay offline for weeks can replay migrations sequentially into their isolated schema upon re-establishing connection.
- Disaster Isolation: Point-in-time recovery can be executed on a single tenant's schema without rolling back changes made by any other customer.
4. Reconciliation Without Spinners
What happens when two registers sell the last box of medication while disconnected from each other?
Traditional software freezes or errors. NoreOS applies compensating business logic: both sales complete because the physical customer is holding the product. Upon reconciliation, the central engine records a negative inventory variance, creates an audit flag, and notifies the store manager's restocking dashboard.
Key Takeaway
Offline-first is not a UI state; it is a fundamental commitment to event sourcing, immutable append-only logs, and causal reconciliation. When the data model respects disconnection from the start, a network outage becomes just another routine delay in event transit.