My father runs a cyber café — one of the small service shops across India (CSCs, cyber cafés, Xerox/DTP counters) that resell government or utility services: Aadhaar updates, PAN cards, printing, bill payments, exam forms. The business model is simple: charge the customer a fixed fee, pay a smaller cost to the portal/vendor that actually processes it, keep the difference.
The problem is just as simple, and just as easy to miss: he could always tell you how much cash came in for the day. He could almost never tell you how much of that was actually profit, because that means subtracting a different portal cost for every single service, every single transaction, in his head, at the end of a long day.
So I built Khata ("ledger" in Hindi/Bengali) to do that subtraction for him, automatically, per transaction, in real time.
What it does
- Define each service once — name (English/Hindi/Bengali), an emoji icon, a default customer charge, and a default portal cost
- Log a transaction in two taps: pick the service, the charge/cost pre-fill, adjust quantity if it's a bulk job (e.g. 100 photocopies in one entry instead of 100 separate rows)
- Dashboard shows today's profit, collections, cost, and transaction count at a glance
- Reports over today/week/month/custom range with a 7-day profit trend chart
- Expense tracking (rent, electricity, stationery) separate from service transactions
Full UI in English, Hindi, or Bengali
The stackFrontend: React + Vite + TypeScript + Tailwind, i18next for the three locales
Backend: Node.js + Express + TypeScript
Auth: JWT in a signed httpOnly cookie — no client-side token handling at all
Database: Postgres via Supabase, but only ever touched from the backend
A couple of decisions worth mentioning
RLS as a deny-by-default wall, not a per-row policy engine. Every table has Row Level Security enabled with zero policies for anon/authenticated. That's deliberate — RLS-enabled-with-no-policies denies all access by default. The backend connects with the service_role key, which bypasses RLS entirely. So the frontend has literally no path to talk to the database directly, and I didn't have to hand-write a policy for every table/role combination. The backend derives shop_id from the session cookie on every request and scopes every query to it, which is the actual multi-tenancy boundary.
Money math lives in the request path, not the UI. profit is a Postgres generated column (customer_charge - cost_paid), computed once, at the source of truth, instead of every consumer re-deriving it (and risking drift between the dashboard, reports, and the table view).
Quantity as a multiplier, not a loop. Early on the only way to log "100 photocopies" was 100 identical rows. Adding a quantity field that scales charge/cost while staying overridable (for discounts or one-off pricing) removed almost all the friction my father actually complained about.
i18n wasn't a layer bolted on later. Every service name, every label, is stored/rendered per-language from day one, because the actual users of this app are far more comfortable in Hindi or Bengali than English — an assumption a lot of small-business tools targeting India get backwards.
Try it
It's live and free: https://khata-ebon.vercel.app/
Built for one cyber café, but the shape of the problem (charge vs. cost, per transaction, in a language you actually think in) applies to a lot of small shops. Happy to answer questions about any of the above, especially the RLS/multi-tenancy setup — curious how others have handled a similar "backend-only DB access" pattern with Supabase.
Top comments (0)