# How to detect repeat signups without storing raw emails or IPs | Nolgard

> Link repeat signups with per-site keyed hashes of normalized emails and visitor ids, no raw IPs, and expiring links. Steps, code, storage choices and FAQ.

Source: https://nolgard.com/guides/detect-repeat-signups-without-storing-pii/
Last updated: 2026-10-26

[Nolgard](https://nolgard.com/) / [Guides](https://nolgard.com/guides/) / How to detect repeat signups without storing raw emails or IPs

Guide

# 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.id` can 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](https://nolgard.com/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](https://nolgard.com/docs/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](https://nolgard.com/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](https://nolgard.com/docs/clusters/).

## Sources

- [Nolgard docs: privacy engineering](https://nolgard.com/docs/privacy-engineering/)
- [Nolgard docs: privacy and retention](https://nolgard.com/docs/#privacy)
- [GDPR Article 4(5), pseudonymisation (EUR-Lex)](https://eur-lex.europa.eu/eli/reg/2016/679/oj)
- [RFC 2104, HMAC](https://www.rfc-editor.org/rfc/rfc2104)

## Related

- [Stop free-trial abuse](https://nolgard.com/guides/stop-free-trial-abuse/)
- [Repeat accounts](https://nolgard.com/features/repeat-accounts/)
- [Security](https://nolgard.com/security/)

[Read the docs](https://nolgard.com/docs/)[Start free](https://nolgard.com/app/#/signup)
