Security
Your PDFs stay in your browser — with two exceptions we'll name.
The PDF bytes are parsed, edited, redacted, and signed inside the browser tab you have open. For every tool but two, the server never sees them — and most of the time it isn't even contacted. The exceptions are the flows that exist to hand a document to someone else: Send for signature and the QR phone handoff. Both upload the document, encrypted in your browser, and we store the ciphertext for 7 days. The table below and the section after it spell out exactly what that means, including where the key goes.
This page documents what runs where, what we encrypt, and exactly how you can verify our claims yourself.
What stays local, what touches our server
The honest, two-column truth. The left column is the work you do on your PDF; the right column is what (if anything) leaves your browser to do it.
| What you're doing | What leaves your browser |
|---|---|
| Filling a form (manually) | Nothing. PDF + your typed values stay in-page. |
| Signing yourself (one-click) | Nothing. Signature image is rendered + flattened into the PDF locally. (Finishing on your phone instead is the QR handoff — its own row below.) |
| Signing on your phone (the QR handoff) | The whole document, encrypted in this tab, is uploaded so your phone can fetch it — same 7-day storage as Send for signature. The key travels to your phone in the QR code, off the network, but it is also sent to us with the upload. We never save it and never email it on this path. |
| Redacting (manual selection) | Nothing. We draw black rects locally and re-save. |
| Merging, splitting, rotating pages | Nothing. pdf-lib in your browser. |
| OCR (making a scan searchable) | Nothing. Tesseract.js runs in your browser; the trained language data is a static asset served once from our CDN, not your scan. |
| Auto-fill from profile | Form-field names + types (not PDF bytes) and your saved profile data, to compute the mapping. We do not log the request or response. Sent to a third-party AI provider. e.g. a field named "first_name" of type text, and the matching name from your profile |
| Scan for PII | Extracted text spans (not PDF bytes) from the redact tool, to find the PII worth redacting; the coordinates stay client-side. Sent to a third-party AI provider. e.g. "123-45-6789", which comes back tagged as an SSN to black out |
| AI clause review | The extracted document text you submit for review — no coordinates, no PDF bytes. Sent to a third-party AI provider. e.g. "This agreement renews for successive 12-month terms…", which comes back flagged as an auto-renewal |
| Draft a document | The text you type (the parties, terms, and details you describe), to generate the draft. No PDF bytes are sent. Sent to a third-party AI provider. e.g. "Mutual NDA between Acme Inc and a contractor, 2-year term" |
| Field help | A form field's name and type, plus nearby labels, for a one-line explanation of what the field wants. Sent to a third-party AI provider. e.g. "spouse_ssn" / text, plus the nearby label "Part I — Taxpayer Information" |
| Library cited answers (Max) | Your question plus the excerpts that matched it — each excerpt's extracted text and file name. The file name is sent so the answer can cite its source; the embeddings and search index themselves stay in your browser. Sent to a third-party AI provider. e.g. "What's our notice period?", the paragraph that matched it, and the file name "acme-msa.pdf" so the answer can cite it |
| Ask AI about this document | The open document's extracted text — tagged page by page, so an answer can cite the page it came from — plus your question, when you use Summarize, Key points, Ask, Explain, or Translate on it. When you summarize a document's reviewer notes instead, the notes and their author/page labels are what get sent. No PDF bytes are sent. Sent to a third-party AI provider. e.g. "Who are the parties?", plus the document's text carrying the "[[page 3]]" tag the answer cites back |
| PDF agent | The extracted document text and your instruction, plus the page geometry, the form-field names (the names only — never the values you entered), the positions of text on the page, and the shape of any table your instruction refers to, all derived in your browser. The agent uses them to plan the edits; the plan then runs on your device and no PDF bytes are sent. Sent to a third-party AI provider. e.g. "Black out the account number on page 1", plus that page's text and where each line sits on it |
| Translate document | The document's extracted text and the language you picked — page by page, or line by line when you keep the original layout. The translated PDF is built in your browser from the returned text; no PDF bytes are sent. Sent to a third-party AI provider. e.g. "Rechnungsdatum: 14.03.2026", with the target language "Spanish" |
| Local-AI mode (the account setting) | Whatever the AI feature you used would normally send — it still comes to our server. What changes is which model answers it, not whether a request happens: where the deployment has a self-hosted model wired in (OLLAMA_BASE_URL), we hand the text to that instead of the third-party provider. That model is the operator's, not one on your machine. Where the deployment has no such model, the setting does not quietly send you to the cloud instead — the features that check it decline, so its whole effect there is to switch those features off. It is also not all-or-nothing — the PDF agent and Translate refuse to run rather than fall back to the cloud (their text never leaves the page), while features that don't check the setting still reach the provider exactly as above. We deliberately don't print the per-feature list: it has been written down wrong twice. Watch DevTools → Network, which is authoritative for the build you are actually using. |
| Counsel Mode (in the sign + draft tools) | Nothing — on the flows that offer it, the request never reaches us at all. Your browser posts straight to an Ollama you run (localhost by default), so neither our server nor a third-party provider is in the loop. Toggled per-device inside those tools — not in account settings, and not the same thing as the row above, despite both having been labelled "Local AI mode" in the UI. In the sign tool it is a Max feature and adds a ceremony on top: that window is wrapped in a network sentinel asserting no outbound call happened, and the signed PDF carries a WebAuthn-signed attestation recording that. It does not cover every AI feature in the product. |
| Send for signature | Ciphertext only — we store no key. But by default the key rides in the link, and because we email that link for you it does pass through our server and our email provider on the way to your recipient. See below. |
| Cloud import (optional — Drive / Dropbox) | Your OAuth / authentication interaction + a file listing go to the provider you pick; the file is then fetched directly from the provider into your browser. Off by default — only present when the operator configures it, and the bytes never route through our server. |
| Signing in / paying | Email (Better Auth), payment metadata (Stripe). Standard. |
Even on these AI paths, the PDF bytes never leave your machine — only the text and values named above. (Drafting is the one row where the text isn't extracted from a PDF at all: it's the prompt you type.) You can confirm what gets sent by opening DevTools → Network.
How send-for-signature is encrypted — and where the key goes
Send-to-sign is the only flow where your PDF visits our server at all, and it arrives as ciphertext: the encrypting and decrypting happen in the two browsers, never here. The honest complication is the key, which rides in the link we email for you. The whole flow, including that part:
- Your browser generates a random 256-bit AES-GCM key.
- The PDF is encrypted in-page with that key. The plaintext never leaves the tab — the ciphertext and the IV do, and so does the key, in the link (next two points).
- We store the ciphertext on our server (Google Cloud + Neon) for 7 days from when you created the request, after which a daily sweep deletes it. Signing it doesn't delete it sooner, and neither does deleting your account — that takes your name and email off the request and leaves the encrypted document to expire on the same clock.
- The recipient link looks like
https://attachkit.com/sign/<id>#k=<base64-key>. The fragment after#is the key. Because we email that link for you, your browser sends it to us — key and all — and we pass it to Resend, our email provider. So on this path the key does travel through our server, and it ends up sitting in your recipient's mailbox. That is what the link is: the password to the document. - What we never do is store it. The key is written to no database column, to no audit-log entry, and to no request log — our database holds ciphertext and nothing that opens it. We stop the claim there, because that is where it stops being true: the email we sent on your behalf still has the key in the link, and it sits in our account with Resend for as long as they retain it. We would have to go and get it — but we could. So we make no promise about what we could or couldn't be made to hand over. We tell you what we store, and the key isn't it.
- When the recipient opens the link, their browser reads the fragment, downloads the ciphertext, and decrypts in-page. After signing, their browser re-encrypts the signed PDF with the same key and uploads ciphertext — we never see either plaintext.
- Zero-knowledge mode (Max) is the version where the key genuinely never reaches us: if your recipient has published an encryption key at
/account/encryption-keys, we wrap the content key to them with ECDH, the link carries no fragment, and we store only the wrapped blob. It needs your recipient to be an AttachKit user who opted in, so it can't be the default, and it isn't available for anyone-with-the-link sends.
The trade-off, stated plainly. Anyone holding the full link (including the fragment) can decrypt and sign. Treat the link like a password — share via a secure channel, don't paste in public chats, and let pending requests expire when they're no longer needed.
Cryptography in use
- AES-GCM-256 for at-rest PDF payloads on the send-to-sign path. Native WebCrypto (
crypto.subtle), no third-party crypto library to audit. - TLS 1.2+ in transit for every HTTPS request (HSTS preload enforced — see hstspreload.org).
- Session cookies via Better Auth — HTTP-only, Secure, SameSite=Lax. No tracking cookies, no third-party fingerprints.
- No account passwords. Sign-in is passkey / WebAuthn + email one-time-code only — there's no account password to steal. The optional signature-vault passphrase is stretched with PBKDF2-SHA256 and never leaves your device.
- WebAuthn ceremony policy. AttachKit's hardware-key signing ceremony uses
userVerification: "preferred", not"required". This means the authenticator is asked to perform user verification (biometric / PIN) when it can, but a roaming security key without a biometric won't hard-block the sign. The deliberate trade-off is broader hardware compatibility (older YubiKeys, generic FIDO2 tokens) at the cost of strict UV enforcement. If you're using AttachKit for a notarial or high-assurance flow that depends on biometric being captured at sign time, choose a passkey on a platform authenticator (Touch ID, Windows Hello, Android biometric) where UV is intrinsic. The signed clientDataJSON records the UV flag the authenticator actually set, so a verifier can audit this post-hoc. - Content Security headers: HSTS (2-year max-age + includeSubDomains + preload), X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy no-referrer (to harden the fragment-as-key trick against any proxy shenanigans), Permissions-Policy locking down camera / mic / geolocation.
Compliance posture (the honest version)
We're pre-launch and don't yet have third-party attestations. Here's what we do have, and what we don't:
- GDPR / UK GDPR. For your own account data — your login email, the profile you save with us, and analytics — AttachKit is the data controller. We act as a processor only when a business customer uses AttachKit to handle their own end-users' data (covered by our DPA). See the DPA for the full contractual framing, sub-processor list, and international-transfer position.
- CCPA / CPRA. Same posture — the same delete + export endpoints in your account cover your CCPA rights.
- HIPAA. We don't sign BAAs, so don't use AttachKitas a HIPAA environment. Note the reason is commercial, not architectural: for the browser-only tools there genuinely is no PHI here, but if you send a PHI-bearing PDF for signature (or hand one to your phone by QR) we do hold it, encrypted, for 7 days — and in the default mode the key passes through us, so encryption alone doesn't put it out of scope. Without a BAA, that flow isn't one to use with PHI.
- SOC 2 / ISO 27001. Not yet. We're a solo operation and the cost of an audit ahead of revenue doesn't pencil out. We'll start the SOC 2 Type I process when we have enterprise customers actually asking.
- 21 CFR Part 11 / FDA. Not in scope.
- ESIGN Act / eIDAS (simple e-signatures). Yes. PDFs signed via AttachKit are valid simple electronic signatures under both. They are not "qualified electronic signatures" (QES) under eIDAS — that requires a CA-issued certificate, which is a separate roadmap item.
Trust, but verify
Don't take our word for it. Every claim above is something you can check yourself in a few minutes:
1. Open the network tab while you work
DevTools → Network → load https://attachkit.com/app/sign → drop a PDF. Filter by "Fetch/XHR." You'll see the page loads and the PDF.js worker; you will not see your PDF being uploaded. Click Sign, type, save — still no PDF upload. This is the simplest one-minute proof.
2. Inspect the request bodies on AI paths
For Auto-fill, click the API call to /api/autofill → Payload tab. You'll see fields + profile as JSON — no PDF bytes. Same for /api/redact (text spans, no bytes), /api/clause-review (document text, no bytes, no coordinates), and /api/library-answer (matching excerpts + file names, no bytes).
3. Pull the page offline and watch it still work
After loading /app/sign once, open DevTools → Network → throttle to "Offline" → drop a PDF → sign → download. It works. (The AI features will fail, since they need the network; everything else doesn't.)
4. There's no upload request to find
The three checks above are the proof, and they don't need our source: across every flow the Network tab shows no request carrying your PDF, because parsing and editing run in your browser (pdf-lib / pdfjs-dist, fetched once as static assets and then cached). Our AI endpoints accept only the JSON shapes shown above — field/profile data or extracted text — and none accept raw PDF bytes. The wire, not the repo, is the thing you can verify.
Reporting a vulnerability
Found something? Use our contact form and pick “Security vulnerability.” We acknowledge inside one business day and work the fix with you. No formal bug bounty yet, but we'll credit responsibly-disclosed findings if you want.
For pre-launch we don't have a separate security alias, PGP key, or HackerOne program — when those exist, this page is where we'll publish them.