Most trial abuse involves several accounts at once. Trial resets are different: there is only ever one account, it is just never the same one for long. The user signs up, uses the trial, hits the paywall, deletes the account from settings, and signs up again the next day. Their email may be identical each time, because your system forgot it when you deleted the user.
This loop is hard to see in dashboards because the evidence is gone. A deleted user is not in your users table, so no uniqueness check, email normalization or duplicate-detection query will ever fire. It also tends to be a growing problem as products get better at honoring deletion requests, which they should.
The answer is not to stop deleting data. It is to decide, deliberately, what minimal fact must outlive the account, and keep only that.
Why deletion resets everything
A typical deletion handler cascades: user row, workspaces, sessions, billing customer, analytics identity. Trial eligibility is usually computed from that same data, for instance “does a user with this email exist and have trial_used = true”. Once the cascade runs, the answer becomes no.
Some teams patch this by soft-deleting, keeping the full row with a deleted_at timestamp. That fixes the loop and creates a different problem: you are now retaining a full profile for someone who asked you to delete it. Under the GDPR and similar laws, a deletion request generally means the data goes, unless a specific exception applies, and “we wanted to keep everything just in case” is not one of them.
A minimal trial ledger
Separate the question “did this person already receive a free trial” from the account itself. Keep a small table that records only what is needed to answer it:
CREATE TABLE trial_ledger (
email_hash bytea, -- HMAC of the normalized email, keyed with a server secret
visitor_id text, -- Prynt visitorId of the signup device
trial_granted date NOT NULL,
expires_at date NOT NULL -- e.g. trial_granted + 12 months, then purge
);
CREATE INDEX ON trial_ledger (email_hash);
CREATE INDEX ON trial_ledger (visitor_id);
Properties worth copying:
- No profile. No name, no plaintext email, no usage data, no account id that leads back to other tables.
- Keyed hash, not plain hash. A plain SHA-256 of an email can be reversed by hashing a list of candidate addresses. An HMAC with a server-side key cannot be checked without that key. Normalize the email first so
[email protected]and[email protected]hash the same; the Gmail normalization guide has the rules. - An expiry. The purpose is preventing trial resets within a reasonable window, not tracking someone forever. Pick a period that matches your pricing cycle, write it in your privacy notice, and purge on schedule.
Write to the ledger when the trial is granted, not when the account is deleted. Then the deletion handler can cascade freely and the ledger is already in place.
Whether this is permissible for your product is a legal question, not an engineering one. Fraud prevention is commonly cited as a legitimate interest, the GDPR’s recitals mention it explicitly, and the erasure right has limits where the controller has overriding legitimate grounds. But “commonly cited” is not advice for your situation. Document the purpose, the minimal fields and the retention, and have counsel review it. The post on data retention for fraud signals covers how to write that justification.
The device side of the ledger
The email hash catches the user who comes back with the same address. The device id catches the user who comes back with a new one, which is the obvious next move once the first is blocked.
At signup, identify the browser and verify the event server-side. accountsOnDevice lists every account (your linkedId) attached to that device in Prynt, with first and last seen times, independently of your users table:
const event = await prynt.getEvent(requestId); // @prynt/node
const priorByEmail = await ledger.findByEmailHash(hmac(normalizeEmail(email)));
const priorByDevice = await ledger.findByVisitorId(event.visitorId);
const devicesAccounts = event.accountsOnDevice.count; // includes deleted users' ids
const trialEligible = !priorByEmail && !priorByDevice && devicesAccounts === 0;
After creating the user, attach it with updateEvent(requestId, { linkedId: String(user.id) }). When that user is later deleted, their linkedId remains in the device’s history unless you erase it, which is exactly the decision you have to make deliberately.
Erasure requests and device history
Prynt supports right-to-erasure by visitorId or by linkedId, and retention follows your plan. That gives you two honest options when a user deletes their account:
- Account deletion without an explicit erasure request. Delete the account in your system. Keep the minimal ledger row and leave the device history in place for its normal retention, under the same documented fraud-prevention purpose. This is what stops the reset loop.
- An explicit request to erase all personal data. Assess it as you would any erasure request. If you conclude it must be honored in full, erase by
linkedIdin Prynt, delete the ledger row, and accept that this particular user can get another trial. A small number of fully honored requests will not break your economics.
What you should not do is treat “delete my account” and “erase every trace of me” as the same request by accident, in either direction. Make them distinct flows in your product and in your privacy notice. The privacy page and DPA describe how Prynt processes this data on your behalf.
Block, or offer a paid plan?
Someone who deletes and recreates their account to get another trial is telling you something useful: they want the product enough to go through friction for it. Treating them as an attacker wastes that.
A practical policy:
- Returning device or email, no other risk signals. Create the account, skip the trial, and say so plainly: “This device already used a free trial. Pick a plan to continue.” Consider a discounted first month if your pricing allows it.
- Returning device plus automation signals, such as a Prynt
decisionofblockor reason codes likeBOTorTAMPERING. Refuse the signup. This is not a person resetting a trial; it is a script. - Many accounts on one device in a short time. Look at
accountsOnDevice.accountstimestamps. Five accounts in a week with no deletions in between is multi-accounting, not trial resets, and deserves a firmer response.
The message matters as much as the rule. Telling someone their device is recognized, without accusing them, keeps the door open to a sale and makes it easy for the occasional false positive, such as a secondhand laptop, to contact support.
Watch for the related patterns
Trial resets overlap with two adjacent problems. Banned users re-registering use the same mechanics with a different motive, and the response should be stricter. Trial farming is the industrial version, many devices and many accounts, and needs the network and automation signals as well as device history.
Start by adding the ledger write at trial grant time, before you change any enforcement. After a month you will know how many new signups match a previous trial, and whether the paid-plan prompt converts them.
Try it free
Prynt is device intelligence with a free tier — visitor IDs, bot & fraud Smart Signals, and behavioral biometrics, powered by a cross-site network. Start free.