The architecture, in one paragraph
The Excel, CSV, text, JSON and contacts tools are JavaScript that runs on your machine. Your browser reads the file with the File API, the dedupe engine runs in the page, and the download you get back is generated in the tab. There is no upload endpoint for those tools. Not a private one, not a rate limited one: the route does not exist. The engine modules themselves are structurally prevented from running on a server, and that is enforced by a test in the repository rather than by a convention: a server file that imported either engine fails the test suite.
The account layer is separate and small. It knows your email, your plan, hashes of your API keys, and one metadata row per job. The HTTP API is the only place data you send is processed on our infrastructure, and it processes it in memory and answers.
Verify the client-side claim in your own browser
Do not take our word for it. Two tests, both under a minute.
The network panel test
- Open the Excel tool in a fresh tab while signed out.
- Open DevTools and select the Network tab. Leave it recording.
- Drop in the largest file you are comfortable using. Run the dedupe. Download the result.
- Read the request list. You will see the page's own HTML, CSS, JavaScript, the two Google Fonts requests,
and the SheetJS library from
cdn.sheetjs.com. Every one of them is us sending code to you. Nothing goes the other way. Filter to Fetch/XHR for the fastest version of the same proof: signed out, that list stays empty for the whole run.
The airplane mode test
Better, because it cannot be faked by a request you missed. Load the tool page, then turn off your network connection entirely, then drop your file in and dedupe it. It still works. Software that was uploading your file could not do that.
What changes when you are signed in
One thing, and we would rather you find it here than in your network panel. When you are signed in, a
completed browser job posts a metadata record so your dashboard has a history: file name, where it ran,
match mode, the column names you matched on, the fuzzy threshold, rows in, rows kept, rows removed, and how
long it took. It shows up as a single POST to the page's own URL, and you can read its payload in
DevTools: it is a few hundred bytes whether your file had 500 rows or 500,000, because it contains counts
rather than data. The fields map one for one onto the jobs table, which has no column that could
hold a row of your file. Signed out, that request is not made at all.
What the server actually stores
Four tables. Users: email, plan, Stripe customer id, created date. API keys: a hash, a display prefix, a label, timestamps. Jobs: names, counts, timings. API usage: endpoint, mode, counts, billable units, HTTP status, duration.
The verification route is a button, not a promise. Export your account as JSON and read the file. It is a dump of every row we hold about you. Diff it against the schema if you like. There is no field in it that could contain a file, a row, or a cell value, because no table has one.
What happens to a request you POST to the API
The API is the one place you deliberately send us data, so it deserves precision:
- The request body is read into memory, deduplicated, and returned in the response. It is not written to the database, not written to disk, and not sent anywhere else.
- What is recorded is a usage row: which key, the endpoint, the mode, input row count, rows removed, billable units, HTTP status and duration. Nothing from the body itself.
- The raw API key is never persisted or logged. It exists inside the parse of one header and is compared as a hash.
- Requests are refused before the body is read where possible: an oversized
Content-Lengthis rejected without buffering the payload. - The endpoint is CORS-open by design, so browser clients can call it. That is not a security hole. It has no ambient authority: every keyed request must present the key explicitly, and there is no cookie the endpoint trusts.
It still runs on hosted infrastructure, and while it is in flight it exists in our function's memory and passes through our host's network. If that is unacceptable for a given dataset, use the browser tools instead, where it never happens at all.
API keys
- A key is
dd_live_followed by 32 bytes of cryptographically random data, hex encoded. - We store the SHA-256 hash of the key and a 12 character display prefix. We do not store the key.
- The full key is shown exactly once, at creation. If you lose it, no one can recover it for you, including us. Create a new one and revoke the old one.
- Revocation is immediate: a revoked key stops authenticating on the next request.
- Lookup is by hash, so a database dump would not hand an attacker working keys.
- If the database is unreachable, key verification fails closed. An unverifiable key is treated as an invalid key, never waved through.
Sessions
- There is no password anywhere in this system. Sign in is a one time link, valid 30 minutes, sent to your email. Your mailbox is the authentication boundary.
- The session is a single cookie,
dd_session, containing your user id and an HMAC-SHA256 signature over it. It ishttpOnly(so page JavaScript cannot read it),SameSite=Lax(so it is not sent on cross site POSTs), and lasts 90 days. - The signature is compared in constant time, so the check does not leak information through timing.
- A known limitation, stated plainly: there is no server-side session store, so there is no way to revoke one issued cookie. Signing out clears the cookie in that browser only. If you believe a session was stolen, revoke your API keys and delete the account; both take effect immediately. We would rather document this than ship a "sign out everywhere" button that does not do what it says.
Transport and browser hardening
- Everything is served over HTTPS, with certificates managed by our host.
X-Frame-Options: DENY, so the site cannot be framed for clickjacking.X-Content-Type-Options: nosniff.Referrer-Policy: strict-origin-when-cross-origin, so outbound links do not leak the path you were on.- Dashboard pages are marked
noindexand are rendered per request behind the session check.
We do not currently ship a Content Security Policy. Assets come from two third party origins (the SheetJS library and Google Fonts), and a CSP that allowlists them without being tested against every page is decoration rather than a control. It is on the list. This page will say so when it lands, not before.
Third party code in the page
Two origins, both listed in the privacy policy: cdn.sheetjs.com for spreadsheet parsing, and Google Fonts for two typefaces. The SheetJS library parses your file in your browser; it is code we load, not a service we send anything to. If loading a library from a CDN is outside your threat model, the CSV, text and JSON tools do not load it at all.
What you should not trust us with
The useful half of a security page. All of the following is true of the API and the account layer; none of it applies to a browser tool used signed out, where nothing leaves your machine:
- Regulated health data. We do not offer a BAA and we are not a HIPAA business associate. Do not POST PHI to the API.
- Cardholder data. We have never been through a PCI assessment. Card details for our own subscriptions never touch our systems because Stripe Checkout collects them, and that is the only reason it is safe. Do not send card numbers through the dedupe API.
- Credentials and secrets. A list of passwords, tokens or keys is not a deduplication problem worth the exposure. Use the browser tools.
- Anything requiring a signed DPA, data residency guarantees, or an audited control framework. We have no SOC 2 report, no ISO 27001 certificate and no penetration test to share, because we have not had them done. Saying otherwise would be the easiest lie on this page and we are not going to tell it.
- Your only copy of anything. There is no backup of your account we can restore, and account deletion is instant and permanent.
Nothing on this list is a reason not to use the free browser tools, which is where most of this site's work happens. It is a list of things not to route through a hosted endpoint operated by a small company, which is true of any small company, and more useful said out loud.
Reporting a vulnerability
Email support@dedupe.dev with "security" in the subject line. Please include what you found, the steps to reproduce it, and what an attacker could do with it. A proof of concept helps enormously.
- We aim to acknowledge a report within one business day and to tell you what we intend to do about it within a week.
- Test against your own account and your own data only. Do not access, modify or destroy anyone else's, and do not run denial of service tests, automated scanners at volume, or social engineering against us or our providers.
- Give us reasonable time to fix an issue before publishing it.
- We do not run a paid bug bounty. If you report something real and act in good faith, we will not pursue you for it, we will fix it, and we will credit you in the changelog if you want the credit.
Last updated