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:
-
Cloudflare's
request.cfobject — geo data -
The
User-Agentheader — 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"
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'
}
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)
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
);
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;
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)