DEV Community

Max LIAO
Max LIAO

Posted on

How I handle QR code scan analytics without a single external API

When I built my QR code service, I needed scan analytics — device type, location, OS — but I didn't want to pay for analytics APIs or add heavy tracking scripts.

Here's how I get full scan analytics using only what's already available for free.

The setup

Every QR code scan goes through a Cloudflare Worker. The Worker handles the redirect, and as a side effect, it collects analytics data from two free sources:

  1. Cloudflare's request.cf object — geo data
  2. The User-Agent header — device and OS data

No Google Analytics. No Mixpanel. No external API calls.

Geo data from request.cf

Cloudflare automatically attaches location data to every request:

const cf = request.cf
const city = cf?.city         // "San Francisco"
const country = cf?.country   // "US"
const lat = cf?.latitude      // "37.7749"
const lon = cf?.longitude     // "-122.4194"
const region = cf?.region     // "California"
Enter fullscreen mode Exit fullscreen mode

This is available on every Cloudflare Workers request at no extra cost. No ipinfo.io, no MaxMind, no GeoIP database.

Device detection from User-Agent

Instead of a heavy library like ua-parser-js (50KB+), I use simple regex:

function getDeviceType(ua: string): string {
  if (/mobile|android|iphone/i.test(ua)) return 'Mobile'
  if (/tablet|ipad/i.test(ua)) return 'Tablet'
  return 'Desktop'
}

function getOS(ua: string): string {
  if (/iphone|ipad|mac/i.test(ua)) return 'iOS/macOS'
  if (/android/i.test(ua)) return 'Android'
  if (/windows/i.test(ua)) return 'Windows'
  if (/linux/i.test(ua)) return 'Linux'
  return 'Unknown'
}
Enter fullscreen mode Exit fullscreen mode

Is this as accurate as a full UA parser? No. Is it accurate enough for QR code analytics? Absolutely. I care about mobile vs desktop split and top cities — not browser sub-versions.

Async logging

The key insight: analytics logging must never slow down the redirect.

// The redirect fires immediately
ctx.waitUntil(
  logScanEvent(qrCode.id, request, env, request.cf)
)

// User gets their redirect in ~40ms
return Response.redirect(destination, 302)
Enter fullscreen mode Exit fullscreen mode

ctx.waitUntil() tells Cloudflare to keep the Worker alive to finish the database write, but the response is already sent. The user never waits for analytics.

Storage: Supabase

Each scan event is a row in Supabase (PostgreSQL):

CREATE TABLE scan_events (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  qr_id UUID REFERENCES qr_codes(id),
  scanned_at TIMESTAMPTZ DEFAULT NOW(),
  device_type VARCHAR(20),
  os_type VARCHAR(50),
  ip_city VARCHAR(100),
  ip_country VARCHAR(100),
  user_agent TEXT
);
Enter fullscreen mode Exit fullscreen mode

Then I query it with simple SQL for the dashboard:

-- Last 30 days trend
SELECT DATE(scanned_at) as date, COUNT(*) as count
FROM scan_events
WHERE qr_id = $1 AND scanned_at > NOW() - INTERVAL '30 days'
GROUP BY DATE(scanned_at)
ORDER BY date;

-- Device breakdown
SELECT device_type, COUNT(*) as count
FROM scan_events WHERE qr_id = $1
GROUP BY device_type;

-- Top cities
SELECT ip_city, COUNT(*) as count
FROM scan_events WHERE qr_id = $1
GROUP BY ip_city ORDER BY count DESC LIMIT 10;
Enter fullscreen mode Exit fullscreen mode

The dashboard

With this data, I render:

  • 30-day scan trend (bar chart)
  • Device breakdown (pie chart — mobile vs desktop vs tablet)
  • Top 10 cities (horizontal bar chart)
  • Total scans and Last 7 days counters

All from data I collected for free.

Cost

Component Monthly cost
Cloudflare Workers (geo data) $5
Supabase (storage + queries) $0 (free tier)
External analytics APIs $0
Total $5/month

Compare this to ipinfo.io ($99/month for 150K lookups) or MaxMind ($25/month). For a side project doing a few thousand scans, free is the right price.

Tradeoffs

  • City accuracy: Cloudflare's geo is based on the nearest data center, not the user's exact IP. For some VPN users, the city might be wrong. Good enough for analytics.
  • UA parsing: My regex misses edge cases (smart TVs, game consoles). For QR codes, 99% of scans are phones or desktops, so this doesn't matter.
  • No real-time: Data is available after a few seconds (Supabase insert + query). Not truly real-time, but close enough.

Takeaway

Before reaching for an analytics API, check what data you already have. Between Cloudflare's request.cf and the User-Agent header, you get 80% of what paid analytics tools offer — at zero cost.


I built this for OwnQR, a $15 one-time QR code generator. The full architecture is open source: cloudflare-qr-redirect.

Top comments (0)