Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 

Repository files navigation

Case study — Two point-of-sale products from one multi-tenant core

This repository contains no source code. It documents the architecture and the engineering decisions behind two POS products deployed for paying clients. The implementations are private.

Role: sole engineer — discovery with the owners, architecture, implementation, deployment, support. Status: both products deployed; the kiosk POS is running with its first client.


The problem

Two businesses in Mendoza, Argentina, with the same underlying pain and completely different shapes:

  • A bar where the waiter took orders on paper, walked them to the kitchen, and rebuilt the table's bill from memory at closing time.
  • A neighbourhood kiosk selling by barcode and by weight, with a notebook for store credit ("fiado") that nobody could reconcile at the end of the month.

Off-the-shelf POS software existed for both. It was priced per terminal per month in dollars, assumed a permanent internet connection, and none of it modelled store credit the way the neighbourhood actually uses it.

Constraints that shaped the design

  • The bar cannot stop. A POS that fails at 11pm on a Saturday is worse than paper.
  • Connectivity is not guaranteed. The kiosk loses internet regularly; sales cannot stop with it.
  • Argentine tax invoicing (AFIP) is required on demand, not on every ticket.
  • Two products, one codebase. The second product could not be a copy-paste fork left to rot.

Architecture

flowchart TB
    subgraph Clients
      W[Waiter · tablet] --- K[Kitchen display]
      C[Kiosk counter · barcode + scale]
    end
    W --> APP[Next.js app · TypeScript]
    K --> APP
    C --> APP
    APP --> ORM[Drizzle ORM]
    ORM --> DB[(PostgreSQL / PGLite<br/>tenant-scoped)]
    APP --> RT[Realtime channel<br/>orders & tables]
    RT --> K
    APP --> FIS[Tax invoicing · on demand]
    APP --> DOCK[Docker · single container deploy]
Loading

Both products are the same core with different surfaces: table map and kitchen tickets for the bar, barcode scanning and sale-by-weight for the kiosk.

Engineering decisions worth explaining

Money is stored as integer cents, formatted only at the edge. Prices, totals and store-credit balances are integers in the database; floats never touch a currency value. Under high inflation these ledgers get audited by the owner against cash in the drawer, and a rounding drift of a few cents per line destroys trust in the whole system.

One ORM for queries, realtime only for realtime. All reads and writes go through Drizzle with typed schemas. The realtime client is used exclusively for subscriptions — new orders reaching the kitchen, table state changing. Mixing the two produces two sources of truth that disagree under load.

Embedded database as a first-class deploy target. The kiosk POS ships with PGLite on a mounted volume: one container, no database server, migrations applied by the entrypoint. It removes an entire class of "the database is down" incidents for a single-terminal business, and the same schema runs on full PostgreSQL when a client outgrows it.

Tax invoicing is on demand, and its key never touches the repository. Most kiosk and bar tickets do not need a fiscal invoice; forcing one on every sale would slow the counter for nothing. Invoicing is an explicit action, and the signing key is injected as a runtime secret.

Mobile-first, installable. The kiosk interface was rebuilt mobile-first and shipped as an installable PWA. The counter is a phone or a cheap tablet, not a desktop terminal — designing for the desktop first and shrinking it had produced a UI nobody could hit accurately while serving a queue.

Results

  • Bar POS: 16 stories across 5 epics, full automated suite green, QA sign-off before deploy.
  • Kiosk POS: multi-tenant, live with its first client; feedback from that client drove the store-credit reconciliation fix and a daily restocking screen.
  • A product catalogue of ~71,000 barcoded items was assembled from a public retail API so a new kiosk starts with products already loaded instead of an empty database.
  • Second product built on the first core, not forked away from it.

What I would do differently

The first client's feedback exposed a bug that the test suite had blessed: a store-credit payment was not reaching the daily cash count because of a casing mismatch between two payment-method labels — and the fixture asserting the behaviour contradicted the seed data. The suite was green and wrong. I now treat fixtures that diverge from production seeds as a defect in their own right.

About

Case study — two point-of-sale products from one multi-tenant core (Next.js, Drizzle, PostgreSQL/PGLite). Architecture and engineering decisions; no source code.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors