How We Built QRCodeStack's Analytics Dashboard (and Wired It Into Their Ad Spend)

QRCodeStack sells dynamic QR codes — the kind you print once on a menu, a poster or a box, and repoint later without reprinting. Restaurants, event organisers and packaging teams, mostly in India. By mid-2026 the product was generating about 162,000 scans a month across customer codes.

The founder's problem was not the QR codes. It was that every scan landed as a row in a table and nobody could see it. Customers asked "how many people scanned the poster in Koramangala vs. the one in Indiranagar?" and the honest answer was a CSV export and a prayer. And on the company's own side, they were spending ₹87,000 a month on Google Ads with no way to connect a scan back to the ad that caused the sign-up.

We built two things: a customer-facing analytics dashboard, and a pipe from scan events into the company's own ad account. This post is about the first; the second is its own post.

What the dashboard had to do

  • Per-code scan counts, over time, by city, by device, by hour of day
  • Per-campaign roll-ups (a campaign is a set of codes — "Diwali menu inserts", "Bangalore metro posters")
  • Repeat-scan detection (same device scanning twice is a different signal from two people)
  • Fast. Customers open it on a phone, on a 4G connection, in a restaurant kitchen.

And one constraint from the founder: no hard-coded numbers anywhere. Every figure on screen reads from the store. If the sync is broken, the dashboard says so instead of showing yesterday's number as if it were today's. That rule came from us, actually — it is how we build Insightron, and we have been burned by the alternative.

The stack

  • Next.js 16 + React 19 for the dashboard, deployed on Vercel
  • FastAPI for the events API, on Railway
  • Postgres for scan events (append-only, partitioned by month), with a nightly roll-up into a daily_scans table the dashboard reads from
  • Recharts for charts, TanStack Table for the per-code table
  • SWR on the client with a 60-second revalidate on the "today" views

The interesting engineering was in the roll-ups. Raw scan events are cheap to write and expensive to query at 5 million rows. The dashboard never touches the raw table; a cron job (Python, APScheduler) computes daily aggregates per code, per city, per device, and the dashboard reads those. "Today" is the one exception — it reads a materialised view refreshed every minute.

The repeat-scan problem

A QR scan has no user. You get a user-agent, an IP, and a timestamp. We hash (user_agent, /24 of IP, code_id) with a rotating daily salt and call it a "device". Two scans from the same device within 24 hours count as one unique. It is not perfect — a whole office behind one NAT looks like one device — but it is honest, it is explainable to a customer, and it doesn't store anything that identifies a person.

Geography without a geolocation vendor

City comes from a MaxMind-style IP lookup we run in-process. Accuracy in Indian cities is roughly "right metro, sometimes wrong neighbourhood", which is exactly the resolution a poster campaign needs. We say so on the dashboard: "City is approximate."

What it looks like

The customer dashboard has four screens:

  1. Overview — total scans, uniques, top codes, a 30-day sparkline per code
  2. Code detail — scans by hour of day (restaurants love this one: lunch and dinner peaks), by city, by device, and a "first scan / last scan" so you know a poster is still up
  3. Campaigns — roll-ups with a comparison view: two campaigns side by side
  4. Export — CSV, because some customers will always want the CSV

Build time was three weeks, of which one week was the roll-up job and the repeat-scan logic. The UI was fast because the components were already in our design system.

Where it got interesting: scans as conversions

Once the scan data was clean, the founder asked the question that turned this into a growth project: "Can I see which Google Ads campaign brought the customers whose codes get scanned the most?"

That is an offline-conversion problem. A scan happens in the physical world, on a customer's customer's phone, days or weeks after the ad click that caused the sign-up. Google Ads can ingest these as offline conversions if you carry the click id (GCLID) from the original sign-up through to the event.

We did that, and then handed the ad account to Admiral — our paid-ads product — to audit and run. The audit on day one found 40% of the month going to search terms that had never converted, Search Partners left on, and a page-view counted as a primary conversion. Four weeks later, the same monthly budget produced 62% more verified activations. That story is written up on Admiral's own blog: How QRCodeStack cut Google Ads waste by 40%.

Every change Admiral made is a receipt — logged, priced, one-click undo — and the change sequence is also written to a hash-chained record on Auditant, which the founder can verify offline without asking either of us. We think that is the minimum standard for letting software touch someone's ad spend.

What we'd do differently

  • Start with the roll-ups. We built the raw-event API first and the dashboard against it, then had to rebuild the read path when the table hit two million rows. Aggregate from day one.
  • Ship the "sync is broken" state on day one, not day ten. The first time the cron died silently, the dashboard cheerfully showed a flat line for three days.
  • Put the GCLID capture in the sign-up form from the beginning. Retrofitting it meant the first two months of customers can never be attributed.

The numbers

| | Before | After | |---|---|---| | Time to answer "which poster worked" | CSV + spreadsheet, ~1 hour | 10 seconds | | Dashboard load (4G, mobile) | — | 1.1s | | Scan events / month | 162,000 | 162,000 (unchanged — this was measurement, not growth) | | Verified activations / month, same budget | 53 | 86 |


Need a dashboard that reads real data and admits when it doesn't? That's what we build at 4UGUSTA. If the problem is the ad account rather than the dashboard, start with the free audit on Admiral.

A

Augusta Bhardwaj

Full-stack & AI engineer. Building production AI systems at YC-backed startups. Founder of 4UGUSTA Systems — a web development and AI agency.

← Back to all posts