Skip to content
WorkAboutFAQContacthello@rajanna.dev
Case 06 / RETAIL

DilPOS

a multi-tenant POS for Australian corner shops

A point-of-sale and inventory system for small Australian retailers, newsagents first, where the till keeps selling even when the internet drops.

  • Role Full-stack (1 of 3)
  • Timeline 2025, ~4 months
  • Status In production with early retailers
  • Repo
  • 50-table multi-tenant Postgres schema
  • 6 app surfaces in one monorepo
  • Offline-first till that queues sales and syncs on reconnect
POS till screen with product grid and three-item cart
Fig 01The till mid-sale on Register 1 — a Confectionery product grid of real Australian lines with SKUs and prices on the left, three items building in the cart on the right with quantity steppers, and a running GST-inclusive subtotal underneath.

Where this came from

DilPos was the MVP for an Australian startup of the same name. The pitch was narrow on purpose: sell a POS to small retailers who were tired of paying legacy vendors for barcode-list subscriptions and clunky terminals. Newsagents were the wedge — shops that deal with Lotto, newspapers, magazines, smokes, and drinks, all with their own quirks.

I came on as one of three developers and worked across the whole thing. Backend, frontend, deployment, hardware. The scope was wide for a four-month build: a desktop till app, an owner dashboard, a platform-admin console for the DilPos staff who curate product catalogs, a marketing site, an API, and a bridge to physical hardware.

By the end, around 20 shops were running it day to day. They stuck around because it was cheap, it was clean, and it didn't get in the way of a busy counter.

Payment screen with cash denomination buttons and keypad
Fig 02The payment screen tendering a $12.30 sale — Cash, Card, Split and Other methods across the top, an amount keypad, and Australian note buttons from $5 to $100 plus Exact, with an open-till prompt once the cash is counted.

The three things that made this hard

A till screen has no room. The terminal UI had to hold a lot — product search, a live cart, categories, payments, Lotto handling, refunds — on a small screen a cashier taps while a queue builds behind a customer. Every element had to be big enough to hit without looking and reachable without hunting.

Hardware doesn't hold still. Receipt printers, cash drawers, and card terminals change by brand and by firmware version. "Supports printers" is easy to say and painful to deliver.

The internet is not a given. A corner shop's connection drops. When it does, the till cannot stop taking money.

Multi-tenant store admin dashboard with tenant switcher
Fig 03The store admin home for the Railway News Agency tenant — a tenant switcher up top, sales stat cards, a 'Pair till — required' banner and a POS entry button, the back office each newsagent manages on the shared platform.

The build

Deciding the till isn't a webpage — it's an appliance

The till runs as a React app, but it ships as a Windows desktop program wrapped in Tauri, with an MSI installer and an auto-updater. I went with Tauri over Electron because the bundle is smaller and it leans on the OS webview instead of shipping a whole Chromium. For a shop machine that isn't a developer's laptop, a lighter install and a quiet self-update matter more than they sound.

The payoff: the desktop app is a thin shell over the same terminal build that runs in the browser. Most of the "desktop vs web" differences are runtime behavior, not two separate UIs to maintain.

Making hardware someone else's job — on purpose

Instead of teaching the till how to talk to every printer, I split hardware off into its own small service — a hardware bridge that runs locally and speaks to ESC/POS printers and the cash drawer over the LAN. It compiles to a standalone executable and sits next to the till.

I rejected the obvious path of bundling drivers into the desktop app. If I'd done that, every new printer model would mean a new release of the whole till. With a separate bridge, the till just sends a print request to a local address and the bridge deals with the messy part. Card payments through Linkly EFTPOS plug into the same seam. This was the hardest stretch of the project — brand and version mismatches kept breaking things — and isolating hardware behind one service is what finally made it tractable.

Keeping the till alive offline

The terminal caches its product catalog in the browser's own database with Dexie, so lookups and barcode scans work with no network at all. Sales made while offline go into a queue. When the connection comes back, a sync service pushes them up. There's a dedicated model for that queue in the schema and a modal in the UI that shows what's still waiting to sync.

The rule I held to: losing the internet can slow a shop down, but it can never lose a sale. A scanned item checks the local cache first, falls back to an online search only if it misses, and a completed transaction is written locally before anything is sent anywhere.

Catalog as the product, not a barcode dump

The multi-tenant model is where most of the schema's weight sits. Every shop is an Organization with its own Stores, Registers, users, and role-based permissions. On top of that, DilPos staff curate vertical catalog packs — Lotto, newsagent, smokes, drinks — in a separate platform-admin console. A shop subscribes to a pack, adopts its items, and each item is cloned into that store's own product list with its barcode, name, GST, and preferred supplier. DilPos owns the catalog data; the shop owns its stock and prices.

That split is the actual business model. Shops aren't paying for barcodes, which are table stakes. They're paying for the curated newsagent workflows on top.

Add-product wizard matching a barcode against catalog data
Fig 04Step one of the add-product wizard, searching 'Banana' and matching against Open Food Facts and a Coles catalog by barcode — how a shop populates its catalogue from shared product data instead of typing every line by hand.

Under the hood

One npm-workspaces monorepo, six app surfaces: the Tauri desktop till, the React terminal it wraps, an owner dashboard, the platform-admin console, a Next.js marketing site, and a shared package for types and the API client. The backend is NestJS on Prisma over PostgreSQL — a 50-model schema covering multi-tenant orgs, RBAC, sales and payments, inventory, purchase orders, catalog packs, Lotto, and a sync queue. Auth is JWT with a Role/Permission model. BullMQ on Redis handles background jobs; Socket.IO carries realtime notifications; Twilio and Nodemailer cover SMS and email. The hardware bridge is a small Express service on the side.

Add-store wizard with ABN, AUD and Australian timezone fields
Fig 05The add-store wizard's owner-details step, capturing company name, ABN, owner contact, Sydney timezone, AUD currency and Australia as the locked business country — the multi-store setup that stands up a new location under one organisation.

What actually shipped

A working, deployed system that around 20 Australian retailers use to run their counters. The offline till, the multi-tenant catalog, the desktop installer with auto-updates, and printer/drawer integration are all live. Some pieces were still in flight at MVP — EFTPOS through Linkly was being wired in, and the subscription-billing side was on the backlog rather than done. I'd rather say that plainly than pretend the whole roadmap shipped in four months.

POS terminal pairing screen with register ID and code fields
Fig 06The desktop terminal's pairing screen, where a till links to a store by entering an eight-character Register ID and an activation code that expires after fifteen minutes — proof the terminal is a separate device, not a browser tab.

Looking back

If I did it again, I'd write the offline sync tests before the sync code, not after. The offline path is exactly the kind of thing that looks fine in a demo and fails at 5pm on a Friday when a real shop's connection flaps mid-sale — and that's the one place a POS can't afford to be wrong. It worked, but I was hand-testing reconnect scenarios far later than I should have been, and a queue that holds real money deserved a safety net from day one.