[{"data":1,"prerenderedAt":563},["ShallowReactive",2],{"navigation":3,"/blog/case-studies/supermarket-pwa-recovery":51,"/blog/case-studies/supermarket-pwa-recovery-surround":558},[4],{"title":5,"path":6,"stem":7,"children":8,"page":34},"Blog","/blog","blog",[9,13,35,39,43,47],{"title":10,"path":11,"stem":12},"Using the official exchange rate in Venezuela's e-commerce","/blog/bcv-scraper","blog/bcv-scraper",{"title":14,"path":15,"stem":16,"children":17,"page":34},"Case Studies","/blog/case-studies","blog/case-studies",[18,22,26,30],{"title":19,"path":20,"stem":21},"Rebuilding an e-commerce platform on NestJS + Next.js + Medusa in three months","/blog/case-studies/retail-ecommerce-rebuild","blog/case-studies/retail-ecommerce-rebuild",{"title":23,"path":24,"stem":25},"Stabilizing an inherited e-commerce platform on NestJS + Strapi","/blog/case-studies/retail-ecommerce-recovery","blog/case-studies/retail-ecommerce-recovery",{"title":27,"path":28,"stem":29},"A nationwide mobile sales-quotation app, integrated with SAP","/blog/case-studies/retail-sap-quotation-app","blog/case-studies/retail-sap-quotation-app",{"title":31,"path":32,"stem":33},"Shipping a high-scale PWA from an inherited e-commerce project","/blog/case-studies/supermarket-pwa-recovery","blog/case-studies/supermarket-pwa-recovery",false,{"title":36,"path":37,"stem":38},"From Nuxt to Hugo and Back Again","/blog/from-nuxt-to-hugo","blog/from-nuxt-to-hugo",{"title":40,"path":41,"stem":42},"The Intern is downloading movies again","/blog/intern-movie-torrent","blog/intern-movie-torrent",{"title":44,"path":45,"stem":46},"Japanese input in openSUSE Tumbleweed's KDE","/blog/japanese-input","blog/japanese-input",{"title":48,"path":49,"stem":50},"Jhey Pi: an official landing site for an aspiring musician","/blog/jp-landing","blog/jp-landing",{"id":52,"title":31,"author":53,"body":59,"category":533,"client":534,"date":535,"description":536,"extension":537,"featured":538,"image":539,"meta":540,"minRead":541,"navigation":538,"ogImage":539,"outcome_headline":542,"path":32,"repoUrl":541,"role":543,"seo":544,"stack":545,"stem":33,"tags":551,"team_size":519,"type":555,"year":556,"__hash__":557},"blog/blog/case-studies/supermarket-pwa-recovery.md",{"name":54,"username":55,"to":56,"avatar":57},"Iván Álvarez","ivanovertime","https://github.com/ivanovertime",{"src":58,"alt":54},"/avatar.jpg",{"type":60,"value":61,"toc":517},"minimark",[62,67,89,93,97,100,111,118,122,182,185,189,194,197,237,240,244,247,283,286,290,297,333,336,340,343,379,382,386,389,418,421,425,428,501,504,507],[63,64,66],"h2",{"id":65},"tldr","TL;DR",[68,69,70,74,82],"ul",{},[71,72,73],"li",{},"A Node.js + Angular e-commerce project at a regional supermarket chain had reached pre-launch with no reliable deploy path.",[71,75,76,77,81],{},"I joined as the second engineer on a two-person team, finished the platform, and shipped it as a ",[78,79,80],"strong",{},"PWA + mobile apps to a high-volume customer base",".",[71,83,84,85,88],{},"Along the way we built the ",[78,86,87],{},"Docker image registry and CI/CD pipeline"," that made continuous delivery possible — for this project and the ones that followed.",[63,90,92],{"id":91},"context","Context",[94,95,96],"p",{},"A regional supermarket chain in Venezuela had an e-commerce initiative in flight when I joined: there was a Node.js backend, an Angular frontend, and a customer base waiting for it. What was missing was the path from \"code in a repo\" to \"an app customers actually use.\"",[94,98,99],{},"The shape of the work is one any engineer who has joined a pre-launch greenfield will recognise:",[68,101,102,105,108],{},[71,103,104],{},"A codebase that ran in development but didn't yet have a repeatable build or deploy path.",[71,106,107],{},"An open question about the customer surface — PWA, native apps, or both.",[71,109,110],{},"Adjacent business needs (customer segmentation, exchange-rate ingestion) waiting on someone with the bandwidth to pick them up.",[94,112,113,114],{},"The brief was simple in one sentence and hard in practice: ",[115,116,117],"em",{},"finish it and ship it.",[63,119,121],{"id":120},"constraints","Constraints",[123,124,125,138],"table",{},[126,127,128],"thead",{},[129,130,131,135],"tr",{},[132,133,134],"th",{},"Constraint",[132,136,137],{},"Reality",[139,140,141,150,158,166,174],"tbody",{},[129,142,143,147],{},[144,145,146],"td",{},"Team size",[144,148,149],{},"2 engineers total, including me, embedded with the business",[129,151,152,155],{},[144,153,154],{},"Calendar",[144,156,157],{},"Months, not quarters — the chain had already waited too long",[129,159,160,163],{},[144,161,162],{},"Stack inherited",[144,164,165],{},"Node.js backend, Angular frontend, no deploy story",[129,167,168,171],{},[144,169,170],{},"Customer surface",[144,172,173],{},"Needed to reach customers on phones, fast, without an app-store gauntlet",[129,175,176,179],{},[144,177,178],{},"Adjacent work",[144,180,181],{},"Customer segmentation and exchange-rate ingestion were also on the table",[94,183,184],{},"The non-negotiable: ship something customers could use, then build the platform underneath that made the next thing easy.",[63,186,188],{"id":187},"decisions","Decisions",[190,191,193],"h3",{"id":192},"decision-1-ship-a-pwa-first-native-apps-second","Decision 1 — Ship a PWA first, native apps second",[94,195,196],{},"The fastest path to a phone screen wasn't an iOS/Android binary — it was a Progressive Web App that customers could install from a link.",[123,198,199,212],{},[126,200,201],{},[129,202,203,206,209],{},[132,204,205],{},"Option",[132,207,208],{},"Trade-off",[132,210,211],{},"Chose",[139,213,214,224],{},[129,215,216,219,222],{},[144,217,218],{},"Build native iOS + Android apps from day one",[144,220,221],{},"Months of platform-specific work; app-store review on every change",[144,223],{},[129,225,226,229,232],{},[144,227,228],{},"Ship a PWA from the existing Angular code, wrap as mobile apps later",[144,230,231],{},"Web-first surface available in days, not months; one codebase to maintain",[144,233,234],{},[235,236],"decision-check",{},[94,238,239],{},"The mobile apps came after, on the foundation the PWA had already proven.",[190,241,243],{"id":242},"decision-2-build-the-deploy-path-before-finishing-the-features","Decision 2 — Build the deploy path before finishing the features",[94,245,246],{},"Tempting as it was to chase the feature backlog, the bottleneck wasn't features — it was that nothing could be deployed reliably.",[123,248,249,259],{},[126,250,251],{},[129,252,253,255,257],{},[132,254,205],{},[132,256,208],{},[132,258,211],{},[139,260,261,271],{},[129,262,263,266,269],{},[144,264,265],{},"Hand-deploy until launch, automate later",[144,267,268],{},"Faster on day one; every deploy after that costs the same hour",[144,270],{},[129,272,273,276,279],{},[144,274,275],{},"Stand up a private Docker registry + a thin CI/CD pipeline first",[144,277,278],{},"Two weeks of platform work before any new feature shipped; every deploy after that was free",[144,280,281],{},[235,282],{},[94,284,285],{},"Same principle as similar rescue work: stabilize the runtime before chasing the roadmap. The pipeline outlived the project — the team after me kept using it for the Node.js and Angular services that came next.",[190,287,289],{"id":288},"decision-3-treat-customer-segmentation-as-a-small-useful-side-quest","Decision 3 — Treat customer segmentation as a small, useful side-quest",[94,291,292,293,296],{},"Marketing wanted to know which customers to talk to and when. I ran an ",[78,294,295],{},"RFM analysis"," (Recency, Frequency, Monetary) on the order data the new platform was already collecting.",[123,298,299,309],{},[126,300,301],{},[129,302,303,305,307],{},[132,304,205],{},[132,306,208],{},[132,308,211],{},[139,310,311,321],{},[129,312,313,316,319],{},[144,314,315],{},"Wait until a dedicated data team exists",[144,317,318],{},"Months of waiting on insight the business needed now",[144,320],{},[129,322,323,326,329],{},[144,324,325],{},"Run RFM on the existing data, hand the segments to marketing",[144,327,328],{},"Lightweight, immediately useful, and proved the new platform's data was worth something",[144,330,331],{},[235,332],{},[94,334,335],{},"It wasn't a \"data platform.\" It was a one-engineer analysis that paid for itself the first week marketing used it.",[190,337,339],{"id":338},"decision-4-automate-exchange-rate-ingestion-instead-of-typing-it-in","Decision 4 — Automate exchange-rate ingestion instead of typing it in",[94,341,342],{},"Pricing in Venezuela depends on the official exchange rate, which someone on the team was copy-pasting daily. Manual rate entry was both error-prone and beneath the pay grade of every person doing it.",[123,344,345,355],{},[126,346,347],{},[129,348,349,351,353],{},[132,350,205],{},[132,352,208],{},[132,354,211],{},[139,356,357,367],{},[129,358,359,362,365],{},[144,360,361],{},"Keep doing it manually",[144,363,364],{},"Free until it isn't — a single typo moves prices on a whole catalog",[144,366],{},[129,368,369,372,375],{},[144,370,371],{},"Scrape the official rate on a schedule and push it into the platform",[144,373,374],{},"A small ingestion job; removes a daily chore and a class of pricing bugs",[144,376,377],{},[235,378],{},[94,380,381],{},"This is the seed of the BCV-scraper work I later wrote about publicly. Small automations like this one tend to pay back quickly, mostly because they remove a recurring human failure mode rather than because they save time directly.",[63,383,385],{"id":384},"outcome","Outcome",[94,387,388],{},"By the end of the engagement:",[68,390,391,397,404,411],{},[71,392,393,394,81],{},"The platform shipped as a ",[78,395,396],{},"PWA + mobile apps used by a high-volume customer base",[71,398,399,400,403],{},"The company had a ",[78,401,402],{},"private Docker registry and a working CI/CD pipeline"," for Node.js and Angular services — the foundation for everything that came after.",[71,405,406,407,410],{},"Marketing had ",[78,408,409],{},"RFM-based customer segments"," they could act on, drawn from the platform's own data.",[71,412,413,414,417],{},"Pricing ran on an ",[78,415,416],{},"automated exchange-rate feed"," instead of a daily copy-paste.",[94,419,420],{},"In hindsight, the sequencing mattered more than the headcount: getting the deploy path in place before chasing the feature backlog is what made the rest of it shippable by a small team.",[63,422,424],{"id":423},"what-id-do-on-gcp-today","What I'd do on GCP today",[94,426,427],{},"Same brief today, same team size, same constraints. The instincts wouldn't change; the targets would.",[68,429,430,444,458,468,482,491],{},[71,431,432,435,436,439,440,443],{},[78,433,434],{},"Frontend:"," ship the PWA the same way, but host it on ",[78,437,438],{},"Firebase Hosting"," or ",[78,441,442],{},"Cloudflare Pages"," with a CDN edge in front. Lighthouse perf as a CI gate, not an afterthought.",[71,445,446,449,450,453,454,457],{},[78,447,448],{},"Backend:"," containerize the Node.js services for ",[78,451,452],{},"Cloud Run",". No private registry to maintain — ",[78,455,456],{},"Artifact Registry"," comes with the platform.",[71,459,460,463,464,467],{},[78,461,462],{},"CI/CD:"," ",[78,465,466],{},"GitHub Actions → Cloud Run",", preview environment per PR. The pipeline I hand-built in 2022 is now ten lines of YAML.",[71,469,470,473,474,477,478,481],{},[78,471,472],{},"RFM and segmentation:"," land the orders in ",[78,475,476],{},"BigQuery"," via a Cloud Run job, run the RFM query as a scheduled view, and surface the segments to marketing through ",[78,479,480],{},"Looker Studio"," instead of a CSV. Same analysis, no engineer in the loop after week one.",[71,483,484,463,487,490],{},[78,485,486],{},"Exchange-rate ingestion:",[78,488,489],{},"Cloud Scheduler → Cloud Run job → Pub/Sub → BigQuery + the storefront cache",". The scraper still runs; everything around it stops being a cron on a single VM.",[71,492,493,496,497,500],{},[78,494,495],{},"Mobile apps:"," Capacitor or PWABuilder wrapping the same PWA, signed and shipped through ",[78,498,499],{},"Firebase App Distribution"," for staged rollouts.",[94,502,503],{},"The shape of the work stays the same — deploy path first, customer surface next, the smaller automations after that. GCP just removes a lot of the platform plumbing that used to be the job.",[505,506],"hr",{},[94,508,509],{},[115,510,511,512,81],{},"If you're working through something similar and want a second pair of eyes, I'm happy to talk it through — ",[513,514,516],"a",{"href":515},"/contact","reach out here",{"title":518,"searchDepth":519,"depth":519,"links":520},"",2,[521,522,523,524,531,532],{"id":65,"depth":519,"text":66},{"id":91,"depth":519,"text":92},{"id":120,"depth":519,"text":121},{"id":187,"depth":519,"text":188,"children":525},[526,528,529,530],{"id":192,"depth":527,"text":193},3,{"id":242,"depth":527,"text":243},{"id":288,"depth":527,"text":289},{"id":338,"depth":527,"text":339},{"id":384,"depth":519,"text":385},{"id":423,"depth":519,"text":424},"case-study"," Supermarket Client (LatAm)","2026-05-15T00:00:00.000Z","How a stalled Node.js + Angular e-commerce project was diagnosed, recovered, and shipped end-to-end as a PWA and mobile apps for a high-volume customer base — with a Docker registry and CI/CD pipeline that made continuous delivery possible for the team that came after.\n","md",true,"/blog/case-studies/supermarket-pwa-recovery/featuredImage.jpg",{},null,"Recovered an inherited project and shipped to a high-volume customer base","Programmer",{"title":31,"description":536},[546,547,548,549,550],"Node.js","Angular","PWA","Docker","CI/CD",[552,553,554],"e-commerce","platform-recovery","devops","rescue",2022,"Zit281hmWyGuaOAwzwi3-j9euhPJ-vxb_G8u4PLGoJY",[559,561],{"title":27,"path":28,"stem":29,"description":560,"children":-1},"Architecting and shipping a mobile-first sales-quotation application on Laravel + Filament, integrated with SAP as the source of truth for catalog and pricing, and rolled out to sales teams across the country.\n",{"title":36,"path":37,"stem":38,"description":562,"children":-1},"Why I migrated this site from Nuxt 2 to Hugo and later returned to Nuxt—what changed, what stayed, and the decisions that kept the rebuild manageable.",1779739959173]