← All projects

DailyOS

Retail commerce that keeps selling when the connection drops.

Visit DailyOS View source

DailyOS screenshot
54
Prisma models
16
Storefront endpoints
~640
Unit tests
2026
Since
01

The problem

A shop floor is the worst network environment software has to work in, and it is the one place where losing a write means losing money. A card reader, a phone hotspot and a till in a basement will all drop the connection mid-sale, and the cashier cannot stop serving the queue while the software works out what happened.

The usual answer is to retry, which is precisely how one sale becomes two orders.

02

What I built

A multi-tenant commerce platform on Next.js 16 and Postgres, via Prisma: point of sale, products, inventory, purchase orders, returns, stock takes, suppliers, discounts, loyalty and delivery zones, plus a personal finance module alongside it. Tenancy is a space discriminator carried on 38 of the 54 models and enforced in application code, with every uniqueness constraint written as a compound on the space rather than globally, so two shops can both have a customer at the same email address.

It installs as a PWA against a hand-written Serwist service worker, and commerce reads are restored from IndexedDB while commerce writes queue in an outbox until the connection returns.

A keyed storefront API sits on top: sixteen endpoints authenticated by a per-space key that maps to the tenant, so the caller never names the space it wants. That is what the VKT Bougie storefront writes orders through.

03

Decisions I'd defend

Idempotency is a database constraint, not a cache. Every offline-capable model carries a compound unique on the space and a client-generated request id, and the replay path reads the Prisma unique-violation target to decide what the collision means: an order-number collision retries with a new number, a request-id collision must not retry, because retrying there is exactly how you get two orders for one sale.

Draining the outbox takes a Web Lock. Two browser tabs on one till is normal, and two drainers picking up the same queued sale is a duplicate-order generator. For the same reason a queued order is never deleted automatically, not on repeated failure and not at the attempt cap, only by someone explicitly discarding it: a queued sale is money that has already changed hands.

Offline sales never guess an order number. They carry a provisional identifier derived from their own ULID timestamp, in a separate namespace from real order numbers, so two terminals cannot collide because neither is picking a number.

Money is exact decimal in the database, never float, and totals are recomputed server-side from the space’s own tax configuration rather than trusted from the client. Before that, the same basket could total differently depending on whether it came through the POS or the storefront. The conversion to integer minor units is confined to the one boundary that requires it, the payment provider, where the amount is checked against the verified transaction before the order is written.

The payment webhook resolves its signer by trying each configured secret against the signature, rather than looking up a space first. The provider sends no tenant identifier, so the moment a second space exists alongside the first, selecting one arbitrarily makes every signature check fail, including real orders.

04

What went wrong

The first service worker cached every same-origin GET cache-first into one shared bucket, authenticated payloads included. On a shared till that meant the second cashier to sign in could be served the first one’s orders page. The rewrite scopes the persisted cache to the signed-in user, never caches API or server-component payloads at all, and clears the old caches on activation and on sign-out.

Worth stating plainly: the meal-planning module is not server-backed. Its models and server actions exist, but no page calls them, so it runs on local browser storage against a public recipe API and does not sync across devices. It is a prototype living next to production code, and it is described that way rather than counted as a third module.

05

Where it is now

Live, with the commerce and finance modules in real use and the storefront API serving VKT Bougie. Around 640 unit tests, weighted towards the offline and money paths, with continuous integration gating lint, types, tests and a full build.

The honest gaps: the audit log covers membership and administration only, not commerce mutations; rate limiting is per-instance and resets on a cold start; and per-item inventory movement history is not wired up yet.