Offline-first point-of-sale and inventory PWA for Ethiopian shops. Next.js 15, Dexie, Supabase, Serwist.
# ሱቅ / Suq — Offline-First POS & Inventory
A point-of-sale and stock system for small shops that **keeps working when the internet and the power go out**, then reconciles cleanly when they come back.
Built for Ethiopian retail: Amharic-first UI, ETB pricing, 80mm thermal receipts, runs on a cheap Android tablet behind the counter.
---
## The one architectural idea that matters
Everything else here is ordinary CRUD. This is not:
> **Never sync stock quantities. Sync stock *movements*.**
If two tablets are offline and both sell the last case of water, and each syncs `stock_qty = 11`, one shop's sale silently vanishes. Last-write-wins destroys data.
If instead each device appends `{product, qty_delta: -1}` and stock on hand is `SUM(qty_delta)`, both sales survive and the arithmetic is correct no matter what order they arrive in. Append-only rows commute; absolute values don't.
The same principle covers sales — immutable and append-only, where a mistake becomes a `void` row rather than a deletion — and gives you an audit trail as a free side effect.
`docs/BUILD_GUIDE.md` develops this properly.
---
## Stack
| Layer | Choice |
|---|---|
| Framework | Next.js 15 (App Router) + TypeScript strict |
| Styling | Tailwind CSS v4 |
| Local database | Dexie 4 (IndexedDB) — **the source of truth** |
| Cloud database | Supabase Postgres — a replica, never authoritative |
| Offline shell | Serwist (`@serwist/next`) |
| Barcode | USB scanner (acts as a keyboard) |
| Receipts | CSS `@page` at 80mm |
| Deploy | Vercel |
**Runtime cost: zero.** No per-transaction API, no metered services. A CRUD app with a careful sync layer.
---
## Quick start
**Prerequisites:** Node 20+, a Supabase project (free tier is fine).
```bash
git clone
github.com
cd Suq
npm install
```
Create `.env.local` from the example:
```bash
cp .env.example .env.local
```
Fill in the two values from your Supabase project (Settings → API):
```
NEXT_PUBLIC_SUPABASE_URL=ht …