How to detect repeat signups without storing raw emails or IPs
Short answer. Hash the identifiers you link on, per site, and never keep the raw values. Fold email aliases before hashing, store network identifiers as salted hashes, keep a first-party visitor id, and expire links after 30 days. Nolgard works this way: raw IPs are not stored, links are per-site hashes, and events are deleted after 14 to 90 days.
Last updated 2026-10-26 ยท Written by the Nolgard team
What you need to link, and what you do not need to keep
To say "this is the same person" you only need to know that two signups share a value. You do not need the value itself. A keyed hash of a normalized email answers the question, and a leak of that table exposes far less than a list of addresses.
Steps
- Normalize first. Lowercase, then fold provider aliases: Gmail dots and plus tags, Outlook and iCloud plus tags. Otherwise two hashes of one mailbox never match.
- Hash with a per-site secret. Use HMAC with a key per site, not a bare SHA-256, so the same email on two customers' sites gives two unrelated values and nobody can test guesses against your table.
import { createHmac } from 'node:crypto'; const linkKey = (siteSecret, normalized) => createHmac('sha256', siteSecret).update(normalized).digest('hex'); - Do the same for network data. Keep a salted hash of the IP prefix for counting, never the raw IP. Do not use the network alone to join accounts: an office or a mobile carrier puts many people behind one address. In Nolgard an IP prefix never joins accounts into a cluster.
- Use a first-party visitor id for the browser. One random id in a first-party cookie, localStorage and IndexedDB survives a single cleared store. Nolgard offers
data-storage="none"when your DPO says you cannot store it; linking then falls back to the device fingerprint and email, and catches fewer repeats. - Expire links. Device and email links in Nolgard cover 30 days; events are deleted after 14 days on Free, 30 on Pro and 90 on Business.
- Send Nolgard an opaque account id.
account.idcan be your internal id; the email is optional.// Your signup handler, after the browser sent `token` const res = await fetch('https://nolgard.com/v1/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ secret: process.env.NOLGARD_SECRET, token, remoteIp, action: 'signup', account: { id: newUserId, email } }), signal: AbortSignal.timeout(3000) }); const result = await res.json(); if (!result.success) return createAccount({ share: 0.2 }); // fail open, reduced grant (see /docs/#failures) if (result.decision === 'allow' || result.decision === 'limit') return createAccount({ credits: result.grant.credits ?? result.grant.share }); if (result.decision === 'challenge') return askForEmailConfirmation(); return reject();
Storage choices compared
| Choice | Can link repeats? | Exposure if leaked | Notes |
|---|---|---|---|
| Raw email and IP in your own table | Yes | High: a contact list with IPs | Also falls under every access and deletion request. |
| Plain SHA-256 of the email | Yes | Medium: common emails can be guessed and hashed | Same value across sites and vendors. |
| HMAC with a per-site key, after alias folding | Yes, including aliases | Low without the key | What Nolgard does for emails and visitor ids. |
| Salted hash of IP prefix, counts only | Counts only, never joins | Low | Avoids joining a whole office into one cluster. |
No storage on the device (data-storage="none") | Fewer repeats caught | Nothing stored in the browser | For sites that cannot store an id before consent. |
Questions
Is a hashed email still personal data under the GDPR?
Usually yes: pseudonymised data is still personal data if it can be linked back with extra information. Hashing reduces risk; it does not take you out of scope. See our DPA and ask your DPO.
Does Nolgard use browser fingerprinting?
Yes, alongside a first-party visitor id and the normalized email. Links are stored as per-site hashes. What the script reads and stores is listed in privacy engineering.
Does the script need consent under ePrivacy?
In default mode it stores one visitor id when you call execute(), for fraud prevention on that form. Whether that needs consent on your site is your DPO's call. If yes, use data-storage="none".
Where is the data stored?
In Cloudflare's EU jurisdiction. Subprocessors are listed on subprocessors.
Can I look up which accounts are linked?
Yes. POST /v1/cluster with your secret and an account id returns the linked account ids you sent us. See account clusters.