Skip to main content
Privacy

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
  • A visitor's IP address (we only keep a daily hash of it)

How the daily hash works

Each click's visitor hash combines a secret kept on our servers, the date (in UTC), the visitor's IP address and their browser's user agent, run through SHA-256:

// Simplified from our click recorder
const day = new Date().toISOString().slice(0, 10); // "2026-04-09"
const visitorHash = sha256(`${secret}|${day}|${ip}|${userAgent}`);

Because the date is part of the input, the same visitor gets a different hash every day, so our analytics can't connect a visit on Monday to one on Wednesday. The IP address itself is never written to our database.

What this means for privacy

We designed this to keep as little as possible, but we don't claim it makes the data anonymous:

  • A hash is still an identifier. Someone holding our server secret could recompute a hash for a known IP address and day, so we treat visitor hashes as personal data, not as anonymous counts.
  • Raw click rows are deleted after 90 days. After that, only daily totals remain.
  • Short-link redirects don't set cookies, and there are no advertising pixels on short links or creator pages.
  • Our privacy policy describes what we collect, why, how long we keep it, and the choices you have.

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: across a longer range we show daily unique visitors summed, which counts a returning visitor once per day, not distinct people

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.

For link creators, it means the people who click your links aren't profiled across days or across links. Whether your own use of analytics needs consent or a notice depends on where you and your audience are, so check what applies to you.


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