Tamperlens inspects documents and keeps little: nothing without an account, a closed list of derived fields with one, and the file only if you ask for it. This page describes the actual system, column by column, rather than the usual list of everything a company might conceivably collect.
The short version
- Without an account, the documents you inspect are never written to disk (PDFs, images and Office files alike), and are not kept after the response. No copies, no cache, no backups of them.
- An account keeps a short record of each inspection: hashes, signal ids, the score. Never the file, its name or any text from it. On by default for new accounts, for 90 days; accounts that existed before keep nothing until they turn it on. You can turn it off, or delete it, at any time. Section 1 lists every field.
- The file itself is kept only if your account opts in, encrypted, for 7 to 90 days.
- We never log a document's content, its text, or its filename in normal operation.
- Issuer baselines store a structural fingerprint, never a document. Training documents are read in memory and discarded like any other upload.
- The database holds an account row, a hash of each API key, one ledger line per inspection and the derived records your history kept (section 1).
- Three cookies, all of them ours, and none for advertising or for following you anywhere: your login session, the language you picked, and a signed counter of anonymous checks. Section 4 lists all three.
- Our own page counting happens on our own server and records only a date, a path and a number. Two Cloudflare features (the human check on the free checker and Cloudflare Web Analytics) are the only things any page loads from another domain, and section 4 says exactly what they do and when.
1. Documents you submit for inspection
This section covers every file you send us, whatever its type.
Tamperlens accepts PDFs, images (JPEG, PNG, WebP, GIF, TIFF, HEIF, BMP) and Office
files (.docx, .xlsx, .pptx), and decides
which one you sent by reading the bytes rather than trusting the filename.
When you upload one, to the free checker, the redaction check, the metadata
viewer, the comparison tool, the dashboard test console, or to
POST /api/v1/inspect, POST /api/v1/case,
POST /api/v1/metadata or POST /api/v1/compare. The bytes
are held in memory, parsed on a worker thread, turned into a report, and discarded
when the request finishes. Uploads are capped at 10 MB per file. The file is
never written to disk unless your account opted in to keeping it (below), and
there is no queue or cache behind any of those endpoints.
The report is returned to you. We do not keep the report itself; what an account keeps is the short record described next, never the report's text.
Encrypted PDFs are reported as "not content-analysed" rather than decrypted. We do not attempt to break password protection.
Server logs record the fact of a request (method, path, status, duration), never a document's contents. Filenames are not logged in normal operation.
Issuer baselines change nothing about this. If you train a
baseline (see the dashboard), the documents you
upload for training are parsed in memory and discarded on exactly the same path as
an inspection. They are never written to disk. What is stored is the derived
structural fingerprint: which PDF producer strings, PDF versions, font
sets, page sizes, revision counts and cross-reference styles were seen across your
samples, and how often each. It is a few hundred bytes and it contains no text
from any document (no names, no account numbers, no balances), by design and by a
validator that refuses to store anything else. You can delete a baseline from the
dashboard or with DELETE /api/v1/baselines/:issuer, and it is gone.
What an account keeps: the inspection history
An inspection made for an account, with an API key that belongs to it or from a
checker or the dashboard while you are logged in, through
POST /api/v1/inspect or POST /api/v1/case, can leave a
short record behind. It is what lets you see your history, be told when a file
shares an origin with one your account already sent, and label results. Accounts
created since this history launched have it on by default; accounts that existed
before keep nothing until they turn it on in the
dashboard. The record is a closed list of fields and nothing else:
-
when it was made and when it expires (
created_at,expires_at); -
which endpoint made it (
source, inspect or case), the kind of file (media_type) and the engine version (engine_version); -
a size band (
size_bytes): the size rounded up to a power of two, never the exact size; -
four lineage identifiers of the file, its SHA-256, its permanent PDF
/ID, its creation instant and its font subset tags, each stored only as an HMAC under a random salt of your account (sha256_hmac,id_permanent_hmac,creation_instant_hmac,font_tags_hmac). They let your account recognise the same origin again. Without your account's salt they cannot be checked against a known file, and two accounts' identifiers never match. The salt sits in the same database, so this protects a list of these values on its own, not a copy of the whole database; -
a hash of the issuer profile (
issuer_profile_hash), the structural fingerprint described above. It is not salted: the same layout gives the same hash in any account; -
the score, the band and the reason code
(
risk_score,risk_band,reason_code); -
the ids and severities of the signals that fired
(
signals), never their evidence; -
your policy's verdict and its version, if you sent a policy
(
policy_verdict,policy_version); -
an HMAC of the report body under the same salt
(
report_hash); -
and the bookkeeping: the record id, which is the report id you already have
(
id), your account (user_id) and the key that made the call (key_id, empty for the dashboard).
Never the file, its name, any text from it, a recovered payload, the words under a redaction, an author, a comment or a location. A test reads the database's raw bytes after inspecting files that carry each of those and finds none, and another fails if a column is added without being listed on this page.
Labels. A label you give a record, or one of its signals
(inspection_id, signal_id),
is kept with it: confirmed fraud, genuine, false positive or unsure
(label), and when you gave it. There is no free-text
field.
How long, who sees it, and how to turn it off
- Records are kept for 30, 90 or 365 days, your choice; 90 by default. Shortening the period applies to what is already kept, and lengthening it never extends a record already written. A daily sweep deletes what has expired, and an expired record is never shown, even before the sweep.
-
You can turn it off for the whole account (the dashboard), for one API key (a
key set to
nonethroughPATCH /api/v1/retentionkeeps nothing for any call made with it), or for one call (the headerX-Tamperlens-Retention: none). The key setting and the header can only turn keeping off, never on. The dashboard checker runs on your newest key and follows its setting. - Nothing is ever kept for the anonymous checker or for a key with no account. The metadata viewer, the comparison tool and baseline training keep nothing for anyone.
- The dashboard sees the whole account. An API key sees only the records it made (a rotated key keeps its predecessor's). No one sees another account's records.
-
The "same origin" note on a new inspection (
tenantMatches) compares the file only with records the same caller can see, never across accounts, and it is a fact to review, not a verdict. It travels inside the response the receipt signs, so sharing a whole response shares the ids and dates of those earlier records.
The file itself, only if you opt in
Off by default. An account with a confirmed email address can turn on keeping the uploaded file next to each record, for 7, 30 or 90 days and never longer than the record, so that it can be downloaded or inspected again with a newer engine. Turning it off deletes every file the account kept, at once.
Each file is encrypted with AES-256-GCM under a key of its own, and that key is
encrypted with a master key held in the server's configuration, on the same server.
The encrypted file sits outside the database, and neither it nor its key is in the
database backups described in section 6. The database row
(wrapped_dek, iv,
tag, plus the record id, the account, a size band and
the dates) holds the encrypted key and the encryption parameters, never the
filename. Two limits: the hosting provider's whole-disk images of
the server (Hetzner keeps seven, one a day) contain the encrypted files and the
master key together until each image rotates out; and anyone with full access to
the running server can read the kept files. The encryption protects a copy of the
database or of the disk taken without the server's configuration, not a
compromised server.
Deleting it
From the dashboard or the API you can delete one record
(DELETE /api/v1/history/:id), every record of one file
(DELETE /api/v1/history?sha256=…; the server computes your account's
HMAC), or everything (?confirm=all, from the dashboard only, which
also discards your account's salt). A deleted record goes with its labels and its file:
- it is gone from the database at once, because the database overwrites deleted pages;
- leftover bytes in the database's files on disk are cleared by the next daily sweep at the latest;
- copies in database backups age out within 60 days;
- a kept file is unreadable at once, because its key is deleted with the record, and the encrypted file is removed; the provider's disk images above are the exception until they rotate out.
2. Your account
If you create an account we store, in a single SQLite database:
- your email address, lower-cased;
- a scrypt hash of your password with a per-account random salt, never the password itself, and the hash is not reversible;
- the timestamp at which you confirmed your email address, if you did;
- your Stripe customer id, if you have ever started a checkout, an opaque reference, no card data;
- the date the account was created;
-
your history settings: whether it is on
(
retention_mode), for how many days (retention_days), whether files are kept and for how long (bytes_retention_days), and your account's random salt for the HMACs in section 1 (retention_salt).
API keys: only a SHA-256 hash of each key is stored, alongside its
plan, monthly quota, label, creation date and history setting
(retention: inherit or none). The key itself exists in our systems
for exactly as long as it takes to show it to you once. That is also why we cannot
resend it: rotation issues a new one.
Sessions: logging in creates a row holding a random session id, your account id and an expiry (30 days, extended while you keep using the dashboard). Logging out deletes it; expired rows are deleted automatically.
Email links: "confirm your address" and "reset your password" links are backed by a row holding a SHA-256 hash of the token, its kind, its expiry (7 days for confirmation, 1 hour for a reset) and whether it has been used. Each link works once. Expired rows are deleted automatically.
3. The usage ledger
Every inspection appends one row to a usage log. That row is the entire record of the request:
- a timestamp;
- the size in bytes of the submitted file;
- the risk band of the resulting report (low / elevated / high);
- and either the id of the API key used (for authenticated calls) or the client IP address (for anonymous calls to the free checker), never both.
There is no column for the document, its name, its contents, or any signal it produced. The ledger exists to enforce monthly quotas, to bill correctly, to show you your own usage, and to make the anonymous rate limit auditable.
Retention:
- Anonymous rows (with an IP address): 90 days. An IP address is personal data, so these are the shortest-lived rows in the ledger.
- Keyed rows (with no IP address): 400 days, a year plus slack, so month-over-month comparisons still work.
A daily sweep deletes anything past those limits.
4. Cookies, and what a page loads from other domains
Three cookies. All three are set by us, all three are
SameSite=Lax (a browser never sends them with a request made
by another site. Only when you yourself navigate here, which is what
makes returning from the payment page work) and Secure in production,
and none of them is an advertising or an analytics cookie:
-
tl_session. Set when you log in. It holds a random session identifier and nothing else: no name, no email, no preferences. It isHttpOnly, so JavaScript cannot read it. It expires after 30 days, extended while you keep using the dashboard, and in no case survives 90 days from the login that created it. -
tl_lang: set only when you click PT or EN in the header. It records that one choice, the two lettersptoren, so that the home page opens in the language you asked for instead of guessing from your browser. Merely reading a page never writes it. It is the one cookie that is notHttpOnly, so a script on this site could read it; it holds no identifier, and it lasts a year. Outliving the visit is the entire point of it. -
tl_checks: a counter of how many documents this browser has already checked anonymously, used by the human check described below. It holds a number and an expiry, signed so that it cannot be edited, and nothing that identifies you or your device. It isHttpOnly, lasts seven days, and is written only after a successful anonymous check, only where the human check is switched on.
Why there is no consent banner. Two of the three are strictly necessary: you cannot stay logged in without a session, and an anonymous allowance cannot be counted without a counter. The third is written only in direct response to you clicking a language switch: the "service explicitly requested by the user" case that the ePrivacy rules exempt from consent, and the reason reading a page never sets it. None of the three profiles you, none is shared with anyone, and none can be read by another site. There is no advertising cookie and no analytics cookie here to ask you about.
What a page loads from another domain. Our pages are built from
our own files: the stylesheet and the scripts are served from
tamperlens.com, the typefaces are the ones already on your device, and
no page loads a font, an image or a stylesheet from anywhere else. Two Cloudflare
features are the exceptions. Both are switched on for this deployment today, both
can be switched off without touching a page, and the
Content-Security-Policy header the site sends names their origins and
no others:
-
The human check: Cloudflare Turnstile. The free checker and
the metadata viewer let you inspect documents with no account at all, and that
has to be defended from automated abuse without meeting a first-time visitor
with a puzzle. So nothing happens until your fourth anonymous
check within a week, or sooner if the network you are sharing has already spent
its daily anonymous allowance. Up to that point your browser never contacts
challenges.cloudflare.com: no widget, no script tag, not even the site key, which arrives only in the response that asks you for the check. When it does appear, Cloudflare sees the request and your IP address and answers "human" or "not"; it is a proof-of-humanity check, not an identity check, and it never receives your document. A keyed API call and a logged-in dashboard session are never challenged. -
Cloudflare Web Analytics. Cloudflare's edge adds a small
measurement script to pages on their way to you: served from
static.cloudflareinsights.com, reporting tocloudflareinsights.com. It is not in our HTML and our own server cannot see it. What it measures (the page, the referrer, load timings, and the IP address and coarse location any web request already carries) is processed by Cloudflare under Cloudflare's terms, and reaches us only as aggregate counts in Cloudflare's dashboard. It sets no cookie of ours. Our own counter in section 5 is the one this site actually relies on, and if you block the beacon nothing on the site changes.
Anything else your browser ends up holding from Cloudflare: a bot-management cookie set at the edge, say. Is Cloudflare's, set under Cloudflare's terms as part of carrying the traffic, and is never read by us.
5. How we count page views
We do want to know whether anyone is reading the site. So the server keeps a counter: for each successfully served page it increments one row keyed by (UTC date, page path, campaign tag). This is ours, it runs on our own machine, and it is a different thing from the Cloudflare beacon in section 4.
The campaign tag is about the link, not about you. When a link you
followed carries a ?utm_campaign or ?utm_source label, text the person who published the link chose, identical for everyone who clicks it
: we keep that label beside the path so we can tell which post a page view came
from. We never read utm_term or utm_content, the two
fields marketing tools personalise per recipient; the label is stripped to a short
lowercase token; and the number of distinct labels stored per day is capped, so a
flood of unique labels collapses into one "(other)" row rather than becoming a
fingerprint.
That is the whole record. There is no IP address, no user agent, no referrer, no cookie, no session, no visitor or device identifier, and no way to reconstruct one . A row says "the pricing page was served 14 times on 2026-07-25, three of them from the launch-post link" and cannot say more, because the table has no other columns. We cannot tell whether that was 14 people or one person sixteen times, and we are content with that.
Only the site owner can read these counters, through an endpoint protected by a separate secret token.
6. Who else processes your data
This is the complete list, and it is the authoritative one. The table on the security page is the same list in another shape; if the two ever disagree, this one is the policy. None of these providers receives a document you upload. The only way a document outlives its request is an account's opt-in file retention (section 1): encrypted, on the Hetzner server, and therefore also in Hetzner's whole-disk images of it.
- Hetzner (hosting): the application and its database run on a single virtual server in Hetzner's Helsinki, Finland data centre. Hetzner is a German company and its facilities are in the EU. A snapshot of the database is taken every six hours and stays on that same server's disk, in a directory only the service account can read, alongside Hetzner's own server backups. Once a day a copy of those snapshots is also pulled to a machine controlled by the operator, off the provider, and kept there for sixty days, so losing the server does not also mean losing every backup of it. Once a day the newest snapshot is also encrypted with age on the server, under a key whose private half the server never holds, and sent to Cloudflare R2, where it cannot be deleted for sixty days and is deleted after that. The copies on the server and on the operator's machine are protected by file permissions, not by encryption of their own; the R2 copy is encrypted. All of them carry accounts' history records. They contain no documents: the database never holds one, and the files an account opts in to keeping live outside it and are left out of these snapshots, with their keys. Hetzner's own whole-disk backups of the server are the exception described in section 1.
-
Cloudflare (CDN, proxy, TLS, human check, web analytics): all
traffic reaches us through Cloudflare, which terminates TLS, serves cached static
assets and provides the tunnel to our server. Cloudflare processes request
metadata, including your IP address, under its own terms; we receive the client
IP through the
CF-Connecting-IPheader and use it only as described in section 3. Cloudflare also provides the two page-level features in section 4: the Turnstile human check on the anonymous checker, and Cloudflare Web Analytics. And it stores the encrypted off-site copy of the database backups (R2) described under Hetzner above, which it cannot read. - Stripe (payments), if you subscribe, Stripe collects and processes your payment details and billing information. Card data never touches our servers; we store only Stripe's customer reference. Stripe acts as an independent controller for its own compliance purposes.
- Resend (transactional email): used, when configured, to send the two emails this service sends: address confirmation and password reset. It receives your email address and the message. There is no marketing list and we do not send newsletters.
-
Sentry (error reporting), when a request fails with a server
error, the exception, its stack trace, the route pattern and the HTTP method are
sent to Sentry so that it can be fixed. The payload that produced it does not
go with it: the request body, the cookies, the query string and your IP address
are dropped before the event leaves our process, every credential-bearing header
is replaced with
[redacted], and any API key, session token, email address or IP address that appears inside an error message is redacted on the way out too. Errors a caller should expect (a rejected key, an oversized upload, an exhausted quota) are never reported at all. A scrubbed processor is still a processor, which is why it is named here. - GitHub (source code, CI, deploys). The source lives there and a push runs the tests and triggers the deploy. It holds no production data and nothing about you. It is on the list because code that reaches our server passes through it.
We do not sell or rent personal data, and we do not share it with anyone else except where we are legally compelled to.
7. Why we are allowed to hold this (legal bases)
- Performing our contract with you: your account, your key, the quota ledger, the session, the emails about your account. Without these the service cannot function.
- Your instructions, as part of that contract: the inspection history and any kept files exist only as your account configures them: on or off, for how long, with or without the file. The documents you inspect are often about other people; you decide which ones are checked and why, and we process them on your behalf.
- Legitimate interests: the anonymous rate-limit ledger and the page counters, both of which are the minimum needed to keep the service available and to know whether it is being read.
- Legal obligation. Keeping billing records for as long as tax law requires.
8. Your rights
You can ask us to show you what we hold about you, correct it, export it, or delete it. Write to [email protected] from the address on the account and we will act on it: normally within a few days, and in any case within 15 days.
Deleting your account is done by us on that request; there is no self-service button. It removes your account row, your key hashes, your sessions, your usage ledger entries and everything your account kept: records, labels and kept files. As with any deletion in section 1, copies in database backups age out within 60 days, and the hosting provider's disk images can hold a deleted encrypted file for up to seven days. Two things survive, and it is fair that you know: billing records held by Stripe, which we cannot delete on their behalf and which tax law requires be kept; and the page-view counters, which contain no reference to you at all and so cannot be linked back to you in the first place.
If you are in the EU or the UK you can also complain to your local data-protection authority. In Brazil, the ANPD.
9. Security
- Everything is served over HTTPS; there is no plain-HTTP endpoint.
- Passwords are scrypt-hashed with per-account salts; API keys and email tokens are stored as SHA-256 hashes.
- Files an account opts in to keeping are encrypted with AES-256-GCM, one key per file; section 1 says where the keys live and what that does not protect against.
-
Every page runs under a strict Content-Security-Policy with no inline scripts and
no inline styles. The only origins it allows besides our own are the two
Cloudflare features named in section 4,
challenges.cloudflare.comfor the human check, and Cloudflare Web Analytics' beacon, and each appears in the policy only while that feature is switched on. - The parser treats every file as hostile: hard caps on size and complexity, a wall-clock budget, and execution in a separate worker thread that is killed and replaced if a file takes too long. Embedded JavaScript is flagged, never run.
- The application container runs as a non-root user with a read-only filesystem apart from its data volume, and publishes no ports directly to the internet.
No system is perfect. If you find a vulnerability, please write to [email protected] before disclosing it publicly, and we will work with you.
10. Children
Tamperlens is a developer tool and is not directed at children. We do not knowingly create accounts for anyone under 16.
11. Changes to this policy
The "last updated" date at the top always reflects the current version. If a change materially affects how we handle personal data, we will email account holders before it takes effect.
12. Contact
The data controller is the individual owner of Tamperlens, in Brazil; the about page says who that is. There is no legal entity yet. For any privacy question, request or complaint: [email protected].
Template, not legal advice. This document was written to describe honestly what the software actually does (it was checked against the source code, not copied from another site), but it has not been reviewed by a lawyer, and it does not yet name a legal entity or a representative in the EU. Review it with counsel before relying on it.