Privacy Policy

What we store, what we never store, and who else touches it.

Tamperlens inspects documents and keeps almost nothing. This page describes the actual system, column by column, rather than the usual list of everything a company might conceivably collect.

The short version

  • 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.
  • 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 three things about you: an account row, a hash of your API key, and one ledger line per inspection.
  • 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/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. Nothing is written to disk at any point, and there is no queue, cache or object store behind any of those endpoints.

The report is returned to you and not stored on our side either. If you want to keep it, keep it yourself.

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.

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.

API keys: only a SHA-256 hash of each key is stored, alongside its plan, monthly quota, label and creation date. 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 thing in the database.
  • 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 is HttpOnly, 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 letters pt or en, 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 not HttpOnly, 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 is HttpOnly, 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 to cloudflareinsights.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, because no document outlives the request that carried 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. To be plain about it: in both places those snapshots are protected by file permissions, not by encryption of their own. They contain no documents, because the database never holds one.
  • 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-IP header 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.
  • 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.
  • 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 a month.

Deletion removes your account row, your key hashes, your sessions and your usage ledger entries. 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.
  • 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.com for 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, and tracks the legal entity currently being registered. For any privacy question, request or complaint: [email protected].