DEV Community

Bishnu Saha
Bishnu Saha

Posted on

I Built a Bilingual Profit Tracker for My Father's Cyber Café — Here's the Stack and a Few Decisions Behind It

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 stack

  • Frontend: 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)