A working full-stack build of Thalluvandi: an Express + SQLite backend
(auth, product/vendor browsing, a persistent cart, order checkout with atomic
inventory deduction, the Khata community-credit system, and Sandhai flash-sale
reservations, plus a Socket.IO layer for live stock/order/countdown updates)
serving a real, wired-up frontend (public/index.html + public/app.js) —
the original visual prototype, now talking to the live API instead of mock
data. One process, one port, npm start and you have the whole app.
This is a deliberately scoped-down, runnable implementation of the much larger spec discussed earlier (which called for Kafka, Debezium CDC, PostGIS, a dozen microservices, etc.). That stack needs real infrastructure to stand up — it isn't something that can be "finished" as a folder of files. This app implements the same business rules (unified inventory, per-shop Khata ledgers, atomic Sandhai reservations, real-time sync) on a single process with an embedded database, so you can actually run it today and grow it later. See "Scaling this up" at the bottom.
- Products & search —
public/app.jsfetches/api/products(with live category/search filters) instead of reading a hardcoded array. - Auth — real login and registration against
/api/auth/*, JWT stored inlocalStorage, session restored on reload via/api/auth/me. The "Continue with Passkey" button still simulates WebAuthn visually but signs in with a real API call. - Cart — every add/remove hits
/api/cart; the drawer renders whatever the server returns, so it's consistent across tabs/devices for the same account. - Khata — vendor tags and the cart's Khata box read real account/
application status from
/api/khata. Requesting Khata creates a genuinePENDINGapplication — nothing auto-approves itself anymore. The seeded demo customer already has an active Khata account with Muthu Vegetables & Fruits so you can see the "approved" state immediately; try requesting Khata at any other vendor to see the pending state. - Sandhai — the fire-badge products, ticker, and countdown are driven by
/api/sandhai/liveand the server's real event end-time. At checkout, the frontend best-effort converts Sandhai-flagged cart lines into confirmed reservations via/api/sandhai/:allocationId/holdbefore placing the order. - Orders — "My Orders" now fetches real order history from
/api/orders. - Realtime — a Socket.IO client connects on load and live-updates stock
counts (
inventory:update), Sandhai claim percentages (sandhai:update), and the countdown clock (clock:tick).
- Node.js + Express — REST API
- SQLite (better-sqlite3) — embedded, file-based, zero setup, transactional
- Socket.IO — live inventory/order/Sandhai/clock broadcasts
- JWT + bcrypt — auth
npm install
cp .env.example .env # edit JWT_SECRET before deploying anywhere real
npm run seed # creates thalluvandi.db and loads demo data
npm start # http://localhost:4000Open http://localhost:4000 in a browser — that's the whole app (frontend
- API + WebSocket), served from the one Express process.
Demo login after seeding: phone +919876543210, password password123
(pre-filled in the login modal — just tap Continue). You can also tap
"Create an account" to register a brand-new customer for real.
Reseed from scratch any time with npm run seed:reset.
src/
db/
schema.sql -- full SQLite schema (users, vendors, products,
inventory, cart, orders, khata, sandhai)
connection.js -- opens the DB, applies schema on boot
seed.js -- demo data matching the original frontend mock data
middleware/
auth.js -- JWT auth guard, role guard, vendor-ownership guard
errorHandler.js
routes/
auth.routes.js
products.routes.js
vendors.routes.js
cart.routes.js
orders.routes.js -- checkout transaction lives here
khata.routes.js
sandhai.routes.js
services/
sandhaiReservations.js -- atomic 5-minute hold/release/confirm logic
sockets/
index.js
server.js
All responses are JSON: { status: "success" | "error", data?, message? }.
Authenticated routes expect Authorization: Bearer <token>.
| Method | Route | Body | Notes |
|---|---|---|---|
| POST | /api/auth/register |
{ name, phone, password, email?, role? } |
role defaults to customer |
| POST | /api/auth/login |
{ phone, password } |
returns accessToken |
| GET | /api/auth/me |
— | requires auth |
| Method | Route | Query/Body |
|---|---|---|
| GET | /api/products |
?category=&q=&vendorId= |
| GET | /api/products/:id |
|
| GET | /api/vendors |
|
| GET | /api/vendors/:id |
| Method | Route | Body |
|---|---|---|
| GET | /api/cart |
|
| POST | /api/cart |
{ productId, delta } — delta can be negative |
| DELETE | /api/cart/:productId |
|
| DELETE | /api/cart |
clears the whole cart |
| Method | Route | Body |
|---|---|---|
| POST | /api/orders |
`{ paymentMethod: "COD" |
| GET | /api/orders |
caller's order history |
| GET | /api/orders/:id |
|
| GET | /api/orders/vendor/incoming |
vendor role only |
| PATCH | /api/orders/:id/status |
vendor role only, { status } |
Checkout is a single DB transaction: validates stock for every line, deducts
inventory, debits the vendor-specific Khata ledger if paymentMethod is
KHATA (rejecting if it would exceed the remaining limit), confirms any
Sandhai reservations passed in, writes the order, and clears the cart. If
any step fails, nothing is applied.
| Method | Route | Body |
|---|---|---|
| POST | /api/khata/apply |
{ vendorId, requestedLimitCents } |
| GET | /api/khata |
caller's accounts + pending applications |
| GET | /api/khata/vendor/applications |
vendor role only |
| POST | /api/khata/vendor/applications/:id/decision |
vendor role only, `{ action: "APPROVE" |
| Method | Route | Body |
|---|---|---|
| GET | /api/sandhai/live |
public — active events with live claimed % |
| POST | /api/sandhai/:allocationId/hold |
auth, { quantity } — atomic 5-minute reservation |
| POST | /api/sandhai/reservations/:reservationId/release |
auth — manual release |
Pass the returned reservationId in sandhaiReservationIds on POST /api/orders to convert a hold into a sale; unconfirmed holds auto-release
after 5 minutes.
join:vendor(emit, client→server) — vendor dashboard joins its roominventory:update—{ productId, stockQty }order:created— pushed tovendor:<id>roomorder:status—{ orderId, status }sandhai:update—{ allocationId, soldQty, reservedQty, pctClaimed }clock:tick— server time every 5s, for synced countdowns
public/index.html— the original visual prototype's markup and styles, unchanged, plus a couple of small additions to the login modal (a registration toggle, an inline error slot).public/app.js— a full rewrite of the prototype's inline<script>: every function that used to mutate an in-memory mock array now calls the API above and re-renders from the response. Express serves this folder as static files and falls back toindex.htmlfor any non-/apiroute.
If you want to split the frontend into its own separately-deployed static
site later, just point API_BASE in app.js at the deployed backend URL
and set CORS_ORIGIN in .env to that site's origin.
If you outgrow a single SQLite file:
- Swap
better-sqlite3forpgand pointschema.sqlat Postgres (the schema here was written in plain, portable SQL specifically so that migration is mostly mechanical — add PostGIS only when you actually need geospatial queries). - Move
sandhaiReservations.js's in-memory Map to Redis withSET ... EXso reservations survive process restarts and work across multiple instances. - Only reach for Kafka/Debezium once you have more than one service that needs to react to the same event — a single Express process doesn't need a message bus to talk to itself.