PrivacyApril 14, 2026

Privacy-First Analytics: Tracking Clicks Without Storing IPs

by Qorlivo Team

Every URL shortener needs analytics. How many people clicked your link? From which countries? On which devices? These are basic questions that any link management tool should answer.

But answering them usually means storing IP addresses — and that comes with a mountain of privacy and compliance baggage. GDPR, CCPA, cookie consent banners, data processing agreements... all because you wanted to count clicks.

We took a different approach.

The problem with IP storage

Most analytics systems store raw IP addresses to:

  1. Count unique visitors — if the same IP clicks twice, count it once
  2. Determine geolocation — map IPs to countries/cities
  3. Detect abuse — identify bots and click fraud

The issue is that IP addresses are considered personal data under GDPR. Once you store them, you need:

  • A lawful basis for processing (consent or legitimate interest)
  • A data processing agreement with your hosting provider
  • A privacy policy explaining what you collect and why
  • Data retention policies and deletion procedures
  • The ability to honor data subject access requests

For a URL shortener, that's a lot of overhead. We wanted to skip all of it.

Our approach: daily-rotating hashes

Instead of storing IPs, we hash them with a daily-rotating salt. Here's the flow:

IP address + daily salt → SHA-256 hash → store hash (discard IP)

The daily salt is a random string that changes every 24 hours. This means:

  • Same IP, same day → same hash → counted as one unique visitor
  • Same IP, next day → different hash → no way to link visits across days
  • Hash → IP → computationally infeasible to reverse

We never store the raw IP address anywhere — not in logs, not in the database, not in temporary storage. The hash is computed in memory and the IP is discarded.

How it works in practice

When a click comes in, our recordClick mutation does this:

// Generate today's salt (rotates daily)
const salt = await getDailySalt();

// Hash the IP with the salt
const visitorHash = sha256(`${ip}:${salt}`);

// Store the click with the hash, not the IP
await ctx.db.insert("clicks_raw", {
  linkId,
  visitorHash,
  country: geo.country,    // from Vercel headers, not IP lookup
  device: parsed.device,
  browser: parsed.browser,
  os: parsed.os,
  referrer: parsed.referrer,
  timestamp: Date.now(),
});

The geolocation data comes from Vercel's edge headers (x-vercel-ip-country), not from our own IP lookup. Vercel processes this at the edge and we never see the mapping.

What we can and can't tell you

We can tell you:

  • Total clicks per link
  • Unique visitors per day (via hash deduplication)
  • Country-level geography
  • Device type (mobile, desktop, tablet)
  • Browser and OS breakdown
  • Referrer sources
  • Clicks over time (hourly, daily, weekly)

We can't tell you (by design):

  • Who specifically clicked your link
  • Whether the same person clicked on two different days
  • The exact city or neighborhood of a visitor
  • Any personally identifiable information about clickers

The daily salt rotation

The salt rotation is the key mechanism that prevents long-term tracking:

// Simplified salt rotation logic
async function getDailySalt(): Promise<string> {
  const today = new Date().toISOString().split("T")[0]; // "2026-04-09"
  const existing = await getSalt(today);

  if (existing) return existing;

  // Generate new salt for today
  const salt = crypto.randomUUID();
  await storeSalt(today, salt);

  // Clean up old salts (older than 2 days)
  await purgeOldSalts();

  return salt;
}

Old salts are purged after 2 days. Once a salt is gone, there's no way to regenerate the hashes from that day — even if someone gained access to our database, they couldn't reverse the hashes or link visitors across days.

GDPR compliance

Because we never store IP addresses or any other personal data, our analytics system sidesteps most GDPR requirements:

  • No consent needed — we don't process personal data for analytics
  • No cookie banners — we don't use cookies for tracking
  • No data subject requests — there's no personal data to access or delete
  • No data processing agreements — nothing personal leaves our system

This isn't a legal loophole — it's genuine privacy by design. We architected the system so that personal data never enters our storage layer in the first place.

Trade-offs

This approach isn't perfect. The main trade-offs:

  • No cross-day visitor tracking — we can't tell you if someone visited Monday and came back Wednesday
  • No user journey mapping — we can't track a visitor across multiple links
  • Daily unique counts only — "unique visitors this month" is an approximation (sum of daily uniques, which overcounts)

For most URL shortener use cases, these trade-offs are worth it. If you need full user journey tracking, you probably need a dedicated analytics platform like Plausible or PostHog — and the consent infrastructure that comes with it.

Why this matters

Privacy isn't just about compliance. It's about trust.

When someone clicks a Qorlivo short link, they're not opting into a tracking system. They're just following a link. We think the analytics we provide — click counts, device breakdowns, geographic distribution — are enough to be useful without being invasive.

And for link creators, it means you can share Qorlivo links without worrying about GDPR consent on behalf of your clickers. The privacy is built into the infrastructure.


Try it yourself. Create a short link and watch the privacy-respecting analytics in real time.