Skip to content
Muhammad Moiz

Offline-First POS: Lessons From Building Oscar in React

How an offline-first POS keeps selling when the internet drops: local database, background sync and real-time kitchen screens, from building Oscar.

Muhammad Moiz
Published
min read
4 min read
On this page

A point of sale system has one job that matters more than any feature: it must keep selling. On a busy Friday night a restaurant can't tell a queue of customers to wait until the Wi-Fi comes back.

When I worked on Oscar, a POS for restaurants and retail businesses in Pakistan, "works offline" wasn't a nice-to-have. It shaped the whole architecture. Here's how an offline-first POS works in the browser, and the lessons I'd pass on to any founder planning one.

What "offline-first" actually means

Many apps say they "work offline" when they really mean they show a friendly error. Offline-first flips the usual design:

  • The app reads and writes a local database first. The screen never waits for the server.
  • The server is updated in the background whenever a connection is available.
  • The cloud stays the source of truth for reporting, so owners still see every branch's sales in one place.

For a cashier, the result is simple: punching an order, taking payment and printing a receipt behave exactly the same with or without internet.

The architecture in one picture

Offline-first POS in the browserThe React app reads and writes a local database in the browser, so it works offline. When a connection is available, a sync process sends local changes to the backend, which updates the main database used for reporting.Read / writeSync when onlineReact POS appRuns in the browserLocal databaseWatermelonDBBackend APISync endpointMain databaseReports across branches
The React app reads and writes a local database in the browser, so it works offline. When a connection is available, a sync process sends local changes to the backend, which updates the main database used for reporting.
  • React POS app ↔ Local database: Read / write
  • Local database ↔ Backend API: Sync when online
  • Backend API → Main database

On Oscar, the local database was WatermelonDB running on browser storage, so there was no app to install. The backend received the synced changes and mapped them to the main database.

Lesson 1: design the data before the screens

With a normal web app you can sketch the UI first and add API calls later. Offline-first doesn't forgive that. Every feature started with data modelling: the tables, the relations between them, and the functions that create, update and query records.

An order, for example, isn't one row. It has items, modifiers, discounts and payments, and all of them must be created locally, in one go, without asking the server for IDs. Here's a simplified model to show the idea:

import { Model } from "@nozbe/watermelondb";
import { children, field, date } from "@nozbe/watermelondb/decorators";

export class Order extends Model {
  static table = "orders";
  static associations = {
    order_items: { type: "has_many", foreignKey: "order_id" },
    payments: { type: "has_many", foreignKey: "order_id" },
  } as const;

  @field("type") type!: "dine_in" | "takeaway" | "delivery";
  @field("status") status!: string;
  @date("created_at") createdAt!: Date;
  @children("order_items") items!: unknown;
  @children("payments") payments!: unknown;
}

If the data model is right, the screens are easy. If it's wrong, every new feature fights the sync.

Lesson 2: sync is a product feature, not a background detail

The hardest bugs in offline apps aren't on the screen. They're in the sync: an order that arrives twice, a discount that disappears, two devices editing the same table. What helps:

  • Every record is created with its own unique ID on the device, so the server never has to invent one and duplicates are easy to detect.
  • Changes sync as a batch, so a half-sent order never shows up in reports.
  • The backend owns the rules for conflicts. When two devices change the same thing, one place decides, the same way every time.

Lesson 3: real-time and offline can live together

Kitchens want orders the moment they're punched. On Oscar, orders reached the Kitchen Display System over WebSockets, and prints went to the right printer (receipt, kitchen or bar) through QZ Tray. Each real-time feature needs its own answer to "what happens offline?", decided at design time, not after the first outage.

Lesson 4: test the bad days, not the good ones

An offline-first app needs testing in the conditions it was built for:

  • Turn the network off in the middle of an order, then back on.
  • Punch orders on two devices while one is offline.
  • Close the browser before a sync finishes and reopen it.
  • Fill the local database with a busy week of orders and check the app is still fast.

These tests find the problems that real customers would otherwise find for you.

Is offline-first right for your product?

It adds real work, so it's worth it when people must keep working without a connection: POS systems, field-service apps, inventory and delivery tools. For a typical SaaS dashboard used at a desk, a well-built online app with good error handling is usually enough.

If you're planning a POS or another app that has to keep working when the internet doesn't, you can read how I built Oscar in the case study, or tell me about your project.

About the author

Muhammad Moiz · Senior Full-Stack Engineer

I'm a senior full-stack engineer with 5+ years building production web apps in React, Next.js and Node.js. I build products end to end for founders and small businesses, and take over apps started with AI tools when they need to grow up.

More about me