# Legitimate Interest Assessment (LIA) - Signup guard (disposable email and repeat-device screening)

Assessment for Vevee customers who use the signup guard (`vevee.guard.signup`,
`vevee.guard.checkEmail`, `collectDevice()`) to screen new signups for
disposable emails and repeat devices, under **GDPR Art. 6(1)(f)** (legitimate
interest) and, for the device identifier, an ePrivacy Art. 5(3) analysis.
Keep this document internally for inspection by the supervisory authority.
Recommended review cadence: annually, and whenever the processing changes.

> Not legal advice. The content below describes how Vevee implements the
> processing. Tailor it to your product and have a qualified DPO or counsel
> sign off. Items in **[brackets]** can only be filled by the controller.
> The ePrivacy position in section 3 is defensible but not settled law.

---

## Header

- **Controller**: [Your Company Name]
- **Processor**: Vevee S.r.l. ("APL")
- **Processing activity**: At signup, comparing the new account's email and
  device characteristics, as keyed hashes, with earlier signups of the same
  app, to detect repeat free-tier signups and disposable emails.
- **Date of assessment**: [DATE]
- **Next review**: [DATE + 12 months]
- **Signed off by**: [Role / Name]

---

## 1. Purpose Test

**What is the purpose of the processing?**
Preventing abuse of the free tier at signup: one person creating many
accounts to obtain the free quota again, and signups with disposable or
undeliverable emails. The result is a recommendation (`allow`, `review`,
`deny_free`, `block`) that **you** apply in your own code.

**Is the purpose specific and clearly defined?** Yes. Guard data is used for
abuse prevention at signup and for nothing else. Vevee never reads guard
rows for analytics, funnels, traffic attribution, person profiles, marketing
exports, the Insights API or MCP. All guard code is isolated in one module
and no analytics query touches guard tables. **[Confirm you do not reuse the
verdicts for marketing or profiling.]**

**Is the purpose lawful?** Yes. Preventing fraud and misuse of a service is
recognised as a legitimate interest (Recital 47 GDPR).

**Could the purpose be achieved without this processing?** Not reliably.
Email checks alone miss a person who signs up repeatedly with fresh
addresses. Without any comparison across signups, free-tier farming cannot
be detected. **[Describe your own abuse history or risk if you have one.]**

---

## 2. Necessity Test

**Is the processing necessary to achieve the purpose?** Yes, at signup only.
The guard is called on the signup step. Logins never call it.

**Could the same outcome be achieved with less-invasive means?**
Vevee applies the least invasive form it found:

- **Hashes only.** The email (normalised), the network prefix and the device
  characteristics are stored as `HMAC-SHA256` values keyed with a random
  per-app secret. The raw email, raw user agent and raw fingerprint values
  are not stored.
- **Raw IP in transit only.** The end user's IP address reaches Vevee in the
  body of the `guard/signup` request, is truncated to a network prefix (/24
  for IPv4, /48 for IPv6) and hashed in memory, and is never stored or
  logged. Guard routes never log request bodies.
- **No cross-app matching.** An app only matches against its own signups.
  The per-app secret makes the hashes of two apps unrelated.
- **Third-party lookups see the email domain only.** DNS over HTTPS
  (Cloudflare) and RDAP (rdap.org) receive only the domain part (for example
  `example.com`), never the address.
- **Observe mode first.** The default mode records what the guard would have
  done without enforcing anything, so you can check the false-positive rate
  before it affects a user.
- **Matched accounts are returned as a count only**, never as ids or
  emails.
- **The email domain is stored only when it is public**: disposable email
  services, known throwaway mail servers, relay services and large consumer
  providers (for example `gmail.com`). Private and company domains are never
  stored. The dashboard shows them as "Private domain".
- **The dashboard shows no emails, no private domains and no matched account
  ids**, and offers no export of the flagged or blocked lists. The "Top
  blocked domains" card lists disposable domains with totals only.

**Is data minimisation applied?** Yes: no content, no browsing history, no
cross-site data. The device blob holds these signals, read in the browser at the signup step:
a canvas rendering hash, the WebGL vendor and renderer, an audio context
hash, an installed-fonts mask, screen and hardware characteristics, the
timezone, the language, and the random `localStorage` id `vv_g` (see section
3). On the server Vevee adds the JA4 TLS digest and a reduced user agent
class. All of these are stored only as keyed hashes.

**Retention.** Each guard row expires **180 days** after it was created and
is deleted by a scheduled cleanup. The default lookback for matching is 90
days.

---

## 3. ePrivacy Art. 5(3) and the device identifier

`collectDevice()` reads characteristics of the browser and writes one random
identifier to `localStorage` (key `vv_g`) so the same browser can be
recognised on a later signup attempt. Reading or writing information on the
user's terminal equipment falls under **ePrivacy Directive Art. 5(3)**, and
the EDPB confirms in **Guidelines 2/2023** that this includes
fingerprinting and similar techniques, not only cookies.

Art. 5(3) needs consent unless the access is **strictly necessary** to
provide a service the user explicitly requested. The position this LIA
relies on is that screening for abuse at signup is a security measure
protecting the service the user is signing up for. Be aware:

- This position is defensible, not settled. Authorities differ, and the
  exemption is read narrowly. Fraud prevention for the controller's own
  benefit is not obviously "requested by the user".
- If you do not want to rely on the exemption, collect consent for the
  device step before calling `collectDevice()`, or do not call it. The email
  check works without any device data, and the guard still works with email
  signals alone.
- `collectDevice()` is called only by you, on the signup step. The SDK never
  runs it on its own, on other pages, or after login.

**[State which route you take: strictly necessary exemption, or consent.]**

---

## 4. Balancing Test

**Whose rights and freedoms are affected?**
People who sign up to your app, including legitimate users who share a
network or device (family, office, shared computer, university) and who may
be matched by mistake.

**Reasonable expectations.** Do users expect this processing? Increasingly
yes for abuse screening at signup, once disclosed in your privacy policy
(see `PRIVACY_POLICY_TEMPLATES.md`, section "Signup guard"). Few users
expect a browser identifier in `localStorage`, which is why the disclosure
and the choice in section 3 matter.

**Impact on data subjects.** What is the likely impact?
**Low to moderate.** The worst realistic outcome is a false positive: a
legitimate user is refused the free quota or sent to review. The guard
returns advice, not a decision; a person can still be reviewed by a human,
and in `observe` mode nothing is enforced. A single weak signal (a shared
network) is scored low and does not reach `deny_free` by itself.
**[Describe your appeal path: how a wrongly flagged person reaches you, for
example support email.]**

**Automated decisions (Art. 22).** If you apply `block` or `deny_free`
automatically, consider whether the effect is "significant" for the person
(usually it is not for a free quota, but a refusal to sign up may be).
**[Decide and document: automatic, or human review for `block`.]**

**Vulnerable data subjects.** **[Answer based on your user base. If minors
use the app, say whether the safeguards are still sufficient.]**

**Safeguards in place.**

- Keyed hashes with a per-app secret; no raw IP, raw user agent or raw
  fingerprint values stored.
- No cross-app matching; the purpose is limited to signup abuse.
- 180 day retention, enforced by a scheduled job.
- Erasure and export (see section 5).
- Observe mode, the `review` action, and the per-rule settings let you
  tune the strictness, with a change history on the dashboard.
- Allowlist and blocklist per app to correct mistakes.
- Encryption in transit (TLS) and at rest; DPA with APL covering Art. 28.
- Degraded behaviour is documented (see section 6) so the processing is
  predictable.

**Residual risk.** Shared devices and networks produce false positives. A
hash of a low-entropy value can in theory be tested by whoever holds the
per-app secret, which is why the secret stays server-side and is never
exposed. **[Assess this for your user base.]**

**Conclusion of balancing test.** The legitimate interest in preventing
abuse of the free tier is **not overridden** by the rights and freedoms of
data subjects, given the minimisation, the hashing, the per-app scope, the
bounded retention and the human-reviewable, advisory nature of the output.
**[Confirm or amend.]**

---

## 5. Retention, erasure and export

- **Retention:** 180 days from creation, then hard-deleted.
- **Erasure (default):** `deletePerson(id)` deletes the person's guard rows,
  including rows stored under any alias id of that person, and with them the
  stored public domain.
- **Erasure with `keepGuard`:** `deletePerson(id, { keepGuard: true })`
  (implied by `keepMetering: true`) replaces the end user id of each guard
  row with a random value that links to no person and to no other row, and
  keeps the email and device hashes **until the row's original expiry,
  never extending it**. The stored public domain is cleared: kept rows do
  not carry it. The kept row has no link to any new account; it
  only adds to match scores, so "delete the account and sign up again" is
  still caught. This applies to free and paid users alike.
  **[Only use `keepGuard` if you have assessed this and disclose it in your
  privacy policy. The default erasure deletes everything.]**
- **Export:** `exportPerson` includes the person's guard rows, as hashes
  plus the public domain when one was stored, covering the canonical id and
  every alias id.
- **Right to object (Art. 21):** handle by contacting **[your privacy
  email]**; on a justified objection, erase with the default
  `deletePerson(userId)` **[confirm your procedure]**.

---

## 6. Failure and degraded behaviour

The guard methods never throw. If Vevee is unreachable, the SDK option
`guardUnavailable` (`allow`, the default, or `review`) decides the
recommendation. If Vevee answers but your guard allowance for the period is
used up, the check runs **degraded**: allow and block lists, burner mail
server list, disposable email lists and repeated emails are still checked,
while live DNS and RDAP domain age lookups and device matching are skipped. Your dashboard
setting "When checks are unavailable" (`allow` or `review`) applies to every degraded answer in
which none of those checks fires, and to every answer where Vevee could not run its checks at
all. Metering is never affected. **[State your choice for "When checks are unavailable" and why.]**

---

## 7. Outcome

- [ ] Processing under legitimate interest is appropriate. **Proceed.**
- [ ] Device step relies on consent instead of the strictly necessary
      exemption.
- [ ] Do not use the device step; email signals only.
- [ ] Do not proceed with this processing.

**Signed:** **************\_\_\_**************
**Role:** **************\_\_\_**************
**Date:** **************\_\_\_**************

---

## Notes for the assessor

- This LIA covers the **signup guard** only. Ordinary metering rests on
  performance of contract (Art. 6(1)(b)) and is out of scope. Usage kept
  after erasure is covered by `LIA_SUBSCRIPTION_ABUSE_PREVENTION.md`.
- Test-mode (`sk_test_`) and live signups never match each other.
- The Vevee public disposable-email checker page runs Cloudflare Turnstile
  for Vevee's own visitors, where Vevee acts as controller. It is not part
  of your integration.
- Sub-processors are listed at `/sub-processors`. Only the email domain, not
  the address, is sent to DNS over HTTPS and RDAP lookups.
