I Built a BFIU-Compliant AML Detection System in Python (Here's Why the Kaggle Approach Doesn't Work
The $2,400 Pricing Mistake I Made on My First PWA Gig—and How I Fixed It
- Get link
- X
- Other Apps
AI-generated illustration
Real‑World Hook
It was a rainy Thursday in Gulshan. My inbox pinged: "Need a PWA for my boutique, budget BDT 30,000, deadline 2 weeks". The client, a 28‑year‑old fashion retailer named Riya, showed me her Shopify store loading at 7 seconds on 3G. She’d lost roughly BDT 12,000 in sales last month because customers abandoned carts. I replied, "I can make it offline‑first, under 2 seconds, for BDT 30,000." I signed the contract on Upwork, sent the first invoice, and started building.
The Hidden Problem
Most freelancers think pricing is just a math exercise: hours × rate + buffer. I did the same. I estimated 80 hours, set my hourly rate at BDT 400, added a 10% contingency, and landed on BDT 35,200. I rounded down to BDT 30,000 to look tempting. What I missed was the hidden cost of post‑launch support and the reality of Bangladeshi data plans. Riya’s customers use 3G most of the day, paying BDT 30 per GB. A PWA that still pulls a 5 MB bundle every visit eats her margin.
Technical Breakdown & Logic Flow
Step 1: Audit the existing site. I ran Lighthouse on a 3G throttling profile. Score: 42 % overall, 3 seconds First Contentful Paint, 1.8 seconds Time to Interactive. Step 2: Identify the biggest offenders. The bundle.js was 6.4 MB, images were unoptimized, service worker not caching anything. Step 3: Design a lean architecture.
- Split code with dynamic import()
- Compress images to WebP, serve via Cloudflare Polish
- Use Workbox to precache only the shell (HTML, CSS, critical JS)
- Implement background sync for cart actions
Why this over a monolithic bundle? A monolith would force every visitor to download 6 MB, regardless of connection. The split approach lets a 3G user fetch 200 KB shell, then lazily load product images when scrolling.
Code/Implementation
Below is the service worker registration and Workbox precaching logic I used. First, I explained the reasoning: I need to cache the shell on install, clean old caches on activate, and fallback to network when offline for API calls.
// sw.js – Service Worker entry point
self.addEventListener('install', event => {
// Precache the app shell – tiny, critical assets only
const cacheFiles = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app-shell.js',
'/images/icon-192.png'
];
event.waitUntil(
caches.open('pwa-shell-v1').then(cache => cache.addAll(cacheFiles))
);
});
self.addEventListener('activate', event => {
// Delete old caches that don’t match current version
const keepCaches = ['pwa-shell-v1'];
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.map(key => {
if (!keepCaches.includes(key)) {
return caches.delete(key);
}
})
))
);
});
self.addEventListener('fetch', event => {
const {request} = event;
// Network‑first for API calls, cache‑first for everything else
if (request.url.includes('/api/')) {
event.respondWith(
fetch(request).catch(() => caches.match(request))
);
} else {
event.respondWith(
caches.match(request).then(cached => cached || fetch(request))
);
}
});
// Background sync for cart actions
self.addEventListener('sync', event => {
if (event.tag === 'sync-cart') {
event.waitUntil(syncCart());
}
});
async function syncCart() {
const db = await idb.openDB('cart-db', 1);
const pending = await db.getAll('outbox');
for (const item of pending) {
try {
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify(item),
headers: {'Content-Type': 'application/json'}
});
await db.delete('outbox', item.id);
} catch (e) {
// keep item for next sync attempt
}
}
}
Alternative: I could have used workbox-cli to generate this automatically, but I needed fine‑grained control over the sync tag because Riya’s cart data is sensitive and must survive intermittent connectivity.
Business Application
After deploying, the PWA loaded in 1.2 seconds on 3G, a 40 % improvement. Riya reported a 22 % lift in conversion over the next two weeks, translating to about BDT 2,600 extra revenue per day. That’s BDT 78,000 a month—more than double the price I charged.
How did the pricing mistake factor in? I had quoted BDT 30,000, but the actual effort was 95 hours (including testing on low‑end Android devices). My hourly rate stayed at BDT 400, so the true cost was BDT 38,000. I lost BDT 8,000 on paper, but the client’s revenue boost covered it within a week.
Common Pitfalls & Edge Cases
1. Assuming “offline‑first” means offline only. Users still need fresh data; neglecting a network‑first strategy for API calls leads to stale carts.
2. Ignoring CDN costs. Cloudflare’s free tier is fine for static assets, but large image libraries can blow the budget if not optimized.
3. Hard‑coding cache names. When you forget to bump the version, users keep old assets, causing UI glitches.
4. Skipping device testing. I once missed a Samsung J2 bug where IndexedDB failed on low RAM, causing the app to crash for 12 % of users.
Counterintuitive Insight
The biggest profit driver wasn’t the PWA itself but the post‑launch monitoring contract. I offered a 3‑month SLA for performance audits at BDT 5,000 per month. Riya accepted because the numbers were small, but the SLA gave me a steady cash flow and an excuse to fine‑tune the service worker every month. In hindsight, bundling a maintenance retainer into the original quote would have prevented the under‑pricing mistake.
Conclusion & CTA
Pricing a PWA project isn’t just about hours. It’s about bandwidth, local data costs, and the hidden value of ongoing support. My first mistake taught me to:
- Map every network‑heavy operation.
- Price the maintenance window up front.
- Validate assumptions with real‑world 3G tests.
- Get link
- X
- Other Apps
Comments
Post a Comment