July 15, 2026
5 Bugs My Paying Customers Found Before I Did
What happens when your test users are real shopkeepers with real money.

By Taimoor Azam
4 min read
Hey folks.
Quick context: I built Vaypar, a business management app for small shops in Pakistan. CRM, inventory, POS billing, the whole thing. Twenty shops signed up in the first two weeks, and they pay me actual money every month.
Which means my QA team is a bunch of shopkeepers in Lahore. They don't file GitHub issues. They call me at 9pm and say "the total is wrong."
Here are five bugs they found before I did, and what each one taught me. If you're building anything that touches money, learn from my pain.
1. JavaScript math ate 3 paisa
A shopkeeper calls me. His invoice total ends in ".00000000000001". He is not amused. Neither is his customer, who thinks the software is broken. He's right.
The bug:
0.1 + 0.2 === 0.3 // false. Welcome to JavaScript.0.1 + 0.2 === 0.3 // false. Welcome to JavaScript.Floating point numbers can't represent most decimals exactly. Fine for a physics simulation. Not fine when the number is someone's money and it's printed on a receipt.
The fix: never store money as a float. Store it as an integer in the smallest unit (paisa), do all math on integers, and only format to rupees at display time. In Postgres, the column is NUMERIC, not FLOAT. No exceptions, not even "just this one column."
What it cost me: one very awkward phone call and an evening migrating every money column in the database.
2. The empty dashboard (or: Supabase RLS doesn't care about your feelings)
I enable Row Level Security on a table. Deploy. Ten minutes later: "my products are gone."
They weren't gone. RLS was doing exactly what I told it to, which was nothing, because I enabled RLS and forgot to write the policy. No policy means no rows. For anyone. The data sat there, perfectly safe, perfectly invisible.
The scarier version of this bug is the opposite one: forgetting RLS entirely on a multi-tenant app, so shop A can query shop B's sales. I got the annoying version instead of the catastrophic one purely by luck.
The fix: every table gets RLS enabled and a tenant policy, written in the same migration, before any feature code:
alter table products enable row level security;
create policy "shop members only"
on products for all
using (shop_id = (select shop_id from memberships
where user_id = auth.uid()));alter table products enable row level security;
create policy "shop members only"
on products for all
using (shop_id = (select shop_id from memberships
where user_id = auth.uid()));Then test with the anon key, not the service role key. The service role bypasses RLS, so testing with it proves nothing. Ask me how I know.
What it cost me: ten minutes of panic and one shopkeeper who now double-checks his data every morning.
3. Two customers, one last packet of biscuits
Busy shop, two counters, one item left in stock. Both counters sell it at the same moment. My inventory now says -1.
My original code, which I was proud of:
const item = await getStock(productId); // both read: 1
if (item.stock > 0) {
await setStock(productId, item.stock - 1); // both write: 0... then -1
}const item = await getStock(productId); // both read: 1
if (item.stock > 0) {
await setStock(productId, item.stock - 1); // both write: 0... then -1
}Read, check, write. A race condition so classic it should be in a museum. The window between the read and the write is tiny, until the shop is busy, and then it's not.
The fix: let the database do the check and the write in one atomic statement:
update products
set stock = stock - 1
where id = $1 and stock >= 1
returning stock;update products
set stock = stock - 1
where id = $1 and stock >= 1
returning stock;Zero rows returned means the item is sold out and the sale gets rejected. No window, no race, no negative biscuits.
What it cost me: a stock audit at one shop and my confidence in every read-then-write in the codebase. I went hunting. Found three more.
4. The sales report that lived in London
A shopkeeper insists yesterday's report is missing sales. I check the database. The sales exist. They're just in the wrong "day."
Because my "daily" report grouped by UTC dates, and Pakistan is UTC+5, everything sold between midnight and 5am landed on the previous day. Shops here open late and close late, so this happened constantly. My report was accurate, for a shopkeeper in London.
The fix: store timestamps in UTC (always), but group by the shop's local day when reporting:
select date_trunc('day', created_at at time zone 'Asia/Karachi') as day,
sum(total)
from sales
group by 1;select date_trunc('day', created_at at time zone 'Asia/Karachi') as day,
sum(total)
from sales
group by 1;UTC in storage, local time at the boundaries. Reports are boundaries.
What it cost me: three days of "your software is wrong" before I believed him. He was right. The customer who barely uses computers out-debugged me.
5. Next.js cached my inventory into fiction
A shop updates a price. Refreshes the page. Old price. Refreshes again. Still the old price. Meanwhile the POS is happily billing customers at a price that no longer exists.
Next.js caches fetch results aggressively, and I'd left the defaults on for data that changes every few minutes. Great for a blog. Terrible for inventory.
The fix: be explicit about freshness for anything transactional:
fetch(url, { cache: 'no-store' }); // live data, every timefetch(url, { cache: 'no-store' }); // live data, every timeOr export const dynamic = 'force-dynamic' on routes that must never serve stale data. Cache the marketing pages. Never cache the money.
What it cost me: a few mispriced sales that I covered out of pocket. Cheap tuition, honestly.
Here's the thing I keep coming back to. None of these bugs showed up in my testing. All of them showed up within days of real shops using the product, because real users are chaos generators and chaos finds every gap.
Twenty shops onboarded in fifteen days with zero marketing spend, and the biggest thing they gave me wasn't revenue. It was bug reports money can't buy.
If you're a founder who wants a developer that has already made these mistakes on his own product instead of yours, I'm on Upwork. And if you run a shop drowning in six different apps, that's literally why I built Vaypar.
More war stories coming. The shopkeepers keep finding things.