Kirana Platform: A Modern POS & Inventory System for India's Neighborhood Stores
What it is
Kirana Platform is a point-of-sale and inventory system built for India's kirana stores — the 12-million-plus neighborhood shops that run the country's daily retail. It combines a React billing interface for the counter, a Node.js/Express + Supabase backend, and a Flutter mobile app with customer and delivery experiences. The repository (github.com/Shivay00001/kirana-platform) was verified directly from source: the billing screen, the atomic stock-decrement logic, the schema with seed data, and the PDF invoice generator are all real, working code.
The problem it solves
The kirana store runs on speed and memory. A customer rattles off ten items, the shopkeeper totals it in their head or on a calculator, writes it in a paper ledger, and hopes the stock math works out at month's end. The problems:
- Billing is slow and error-prone — mental arithmetic under rush-hour pressure.
- Stock is invisible — nobody knows what's running out until a customer asks for it.
- No records — no sales history, no invoices, no data to order smarter.
- Double-selling — when two bills compete for the last unit of stock, paper ledgers can't serialize the race.
Kirana Platform's answer is a fast billing screen with live product search, a backend that decrements stock atomically inside the sale transaction, and PDF invoices for every bill — the digital equivalent of the ledger, but one that does the math.
Architecture
kirana-platform/
├── backend/ # Express API on Supabase/Postgres
│ ├── server.js # helmet, rate limiting, CORS, validators
│ ├── src/
│ │ ├── controllers/ # productController, saleController
│ │ ├── routes/ # products.routes, sales.routes
│ │ ├── middleware/ # express-validator input validation
│ │ └── config/ # supabase client
│ ├── schema.sql # shops, products, sales, sale_items + seed data
│ └── rpc_decrement_stock.sql # atomic stock decrement function
├── frontend-web/ # React + Vite POS billing interface
│ └── src/
│ ├── components/Billing/ # BillingScreen, ProductSearch, CartItem
│ ├── components/Inventory/# InventoryScreen
│ ├── utils/ # invoiceGenerator (jsPDF)
│ └── services/ # api, supabase
└── mobile-app/ # Flutter (customer + delivery experiences)
└── lib/
├── screens/ # customer_home, cart, delivery_home
└── services/ # api_service
The billing flow — where the product lives
The core experience is BillingScreen.jsx, a two-pane POS layout: product search and grid on the left, the live bill on the right. The flow:
- Search and add.
ProductSearchqueries products (barcode-aware — the schema has abarcode UNIQUEcolumn); adding increments quantity if the item is already in the cart. Toast notifications confirm each add. - Cart management.
CartItemcomponents support quantity steppers (Math.max(1, quantity + delta)) and removal; the total recomputes in rupees (₹{total.toFixed(2)}) on every change. - Checkout. A single POST to
/api/saleswith{ items, total, payment_method }; on success the sale ID is toasted, the cart clears, and a PDF invoice is generated client-side with jsPDF.
const res = await fetch('http://localhost:5000/api/sales', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: cart, total, payment_method })
});
The invoice generator (utils/invoiceGenerator.js) produces a proper "Tax Invoice": shop header, date in dd/MM/yyyy HH:mm format, invoice ID, an itemized table (item / qty / price / total), grand total, and a thank-you footer — saved as Invoice_<sale_id>.pdf.
The important part: atomic stock decrements
The best engineering in the repo is saleController.js. Recording a sale touches three things — the sales row, the sale_items rows, and product stock — and stock is where races live. Instead of a read-then-write (which double-sells under concurrency), the controller:
- Inserts the sale and batch-inserts sale items.
- Decrements stock per item via a Postgres RPC —
supabase.rpc('decrement_stock', { row_id, qty })— which performs the decrement inside the database with row-level locking.
// 4. Update Stock (Atomicity via RPC)
// This ensures preventing Race Conditions (Double Selling).
// It calls the Postgres function 'decrement_stock' which uses Row-Level Locking.
If any item's stock update fails (insufficient stock or a race), the API returns HTTP 206 Partial Content with the sale ID and a list of failed items — the sale is recorded, the failure is explicit, and the UI can tell the shopkeeper exactly which items need attention. That's a more honest API contract than silently failing or rolling back the entire bill. The repo ships the function itself in backend/rpc_decrement_stock.sql. One caveat worth stating: without the sale insert and the stock RPC wrapped in a single database transaction, a crash between the two could leave a sale recorded with stock not decremented — hardening that boundary is the next correctness step before real rush-hour traffic.
The data model
backend/schema.sql is compact and sensible:
- shops — id, name, address, phone
- products — shop-scoped, with
barcode UNIQUE, decimal price, integer stock, category, image URL - sales — shop-scoped, total, payment method (default
'cash'), timestamp - sale_items — line items with
price_at_sale(so price changes don't rewrite history) andON DELETE CASCADE
It ships with seed data for a demo shop: Maggi Noodles ₹14, Tata Salt 1kg ₹28, Amul Taaza Milk ₹54, Colgate Strong Teeth ₹95 — real Indian SKUs with real EAN barcodes (89010...), which makes the demo feel like an actual kirana counter rather than a Lorem Ipsum store.
Backend hardening
server.js is a properly defensive Express setup:
- helmet for security headers
- Rate limiting: 100 requests per 15 minutes per IP on
/api - CORS restricted to configured origins in production (wildcard only in dev)
- express-validator on the routes for input validation — notably, the code comments explain why
xss-cleanwas removed: it's deprecated and incompatible with Express 5's getter-onlyreq.query. Someone hit the actual bug and documented the fix. - 10kb JSON body limit to blunt payload abuse
- Centralized error handler that logs the stack server-side and returns a generic message client-side (no error leakage)
The mobile app
mobile-app/ is a Flutter app with customer and delivery experiences: customer_home_screen (browse and shop), cart_screen, and delivery_home_screen for fulfillment — the kirana store as an online storefront with its own delivery loop, backed by api_service.dart talking to the same backend. The repo includes per-component READMEs in the Billing and Inventory folders.
Inventory management
The counter app is only half the story. InventoryScreen.jsx gives the shopkeeper stock visibility: products listed with current stock levels, backed by the productController endpoints (GET/POST/PUT /api/products). The products table keeps it practical — barcode UNIQUE for scanner input, decimal prices, integer stock, categories, and optional image URLs. Low-stock awareness is the natural data to surface here: the sale controller already knows when a decrement fails on insufficient stock, so wiring that signal into a restock alert on the inventory screen is a small step with real operational value. The seed.js script (backend/scripts/seed.js) bootstraps the demo shop and products, so a fresh clone is runnable in minutes.
How to run it
git clone https://github.com/Shivay00001/kirana-platform.git
cd kirana-platform
# Backend
cd backend && npm install
# configure .env with SUPABASE_URL and SUPABASE keys
node server.js # http://localhost:5000
# Web POS
cd frontend-web && npm install && npm run dev
# Mobile
cd mobile-app && flutter pub get && flutter run
Apply backend/schema.sql and backend/rpc_decrement_stock.sql to your Postgres/Supabase project first, plus backend/scripts/seed.js for demo data. The backend health check is GET / → {"status":"ok","service":"Kirana POS Backend"}.
Honest status — working MVP with known edges
Status: working MVP, verified as functional code. The billing loop, atomic stock updates, invoice generation, and defensive backend are all real. The known edges:
- Payment method is hardcoded
'cash'in the billing UI (payment_method: 'cash' // Hardcoded for MVP). UPI/card support is the obvious next feature. - Single-shop demo data — the schema is multi-shop, but the seed and controller default to one test shop (
00000000-...-000000000001). - The README is generic boilerplate ("High Performance… Scalable Architecture…") that describes nothing about the actual product. The code is far better than its own documentation — a rewrite of the README is overdue.
- No auth layer on the sales endpoints was visible in the verified files — the kirana frontend is a trusted-counter app, but any network-exposed deployment needs API auth before real use.
Roadmap: UPI payment integration, barcode-scanner input on the billing screen, low-stock alerts, sales analytics for reorder decisions, multi-shop/chain support, and authenticated API access. Longer term, the sales history the system accumulates is itself an asset — reorder suggestions and demand patterns for the shop's top SKUs fall out of data the platform already records.
Why it matters
Kirana digitization is usually pitched as "an app for shopkeepers," which misses the point: the counter is a race — speed of billing, accuracy of stock, trust of the invoice. Kirana Platform gets the one thing right that most POS demos get wrong: the sale is a transaction, not a form post — atomic stock decrements with row-level locking, explicit partial-failure semantics, and immutable price-at-sale records. That's the difference between a demo that looks like a POS and a backend that could survive a real rush hour.
Source: github.com/Shivay00001/kirana-platform · MIT license
Frequently asked questions
What is Kirana Platform?
An open-source point-of-sale and inventory system for India's kirana stores: a React billing interface for the counter, an Express + Supabase backend, and a Flutter mobile app with customer and delivery experiences. MIT licensed.
How does it prevent double-selling?
Stock is decremented inside the database via a Postgres RPC function (decrement_stock, shipped in the repo) using row-level locking, rather than read-then-write. If an item's stock update fails, the API returns HTTP 206 with the failed items listed explicitly.
Does it generate invoices?
Yes — every completed sale generates a client-side PDF "Tax Invoice" via jsPDF with an itemized table, date, invoice ID, and grand total.
What are the known MVP edges?
The payment method is hardcoded to cash in the billing UI, the demo ships a single test shop, the sales endpoints have no auth layer, and the README is generic boilerplate that undersells the actual code. UPI payment integration is the natural next feature.