Problem
Small retail chains need a cashier that keeps selling when the internet drops, prints on cheap thermal printers, handles two currencies, and keeps each shop’s data strictly separate. Off-the-shelf systems were expensive and didn’t speak Uzbek or Russian well.
What I built
The whole platform, alone: a multi-tenant Django backend, an installable Vue web cashier, a native Flutter desktop cashier for Windows and macOS, and a Telegram Mini App storefront. The client’s name and screens stay private.
Front · Web & desktop
- A Vue 3 PWA with an offline-capable POS, 4 languages (Uzbek Cyrillic and Latin, Russian, English) and role-based access.
- A Flutter desktop cashier tuned for low-end hardware: a global USB barcode-scanner buffer, split UZS/USD payments at the daily rate, hold and recall drafts, customer debt.
- ESC/POS receipts with Cyrillic drawn as bitmaps for codepage-limited printers, and TSPL barcode labels over TCP, USB or the OS spooler.
Behind · Servers
- django-tenants gives every shop its own PostgreSQL schema, routed by subdomain, with JWTs bound to their tenant.
- Celery Beat updates exchange rates and expires unconfirmed online orders.
- Automated, encrypted database backups to off-site storage.
Inside · Smart search
Shoppers and cashiers type in either script. A transliteration fold plus a pg_trgm fallback makes “сут” and “sut” find the same product; 3,292 products were backfilled.
Hard problems
- Strict tenant isolation — a self-run security audit hardened the boundary between shops: every token is bound to its tenant and checked on every request.
- Double sales on flaky networks — idempotent sales keyed by a client-side UUID and locked stock mutations.
- 57 confirmed bugs (4 critical) fixed across 29 files in one audit pass.
Results
Four apps on one platform in daily use by the client’s shops.