
Subprocessors
Last updated in the repository as docs/security/SUBPROCESSORS.md.
Every company that processes SignSealer customer data, what each one is given, and where it is.
Status: DOCUMENT ONLY, maintained by hand. Nothing checks that this list matches what the code actually calls. Recorded as an unevidenced control (CC9.2, vendors and business partners; it was filed under CC9.1, business disruption, until the launch review moved it).
Last reviewed: 2026-09-26. The launch review found this list had left out three companies that handle customer data -- Railway, which runs every server process; Vercel, which passes the signing pages and the app through to them; and Sentry, which receives error reports -- and said things the code contradicted ("no payment processor", "no analytics", "no CDN in front of the API"). All three are added below and the rest corrected against the live configuration. Before that: 2026-09-23, Cloudflare Turnstile (00403) and Stripe for the membership and prepaid credit (00402).
Who processes customer data
| Subprocessor | What for | What they see | Where |
|---|---|---|---|
| Supabase | The Postgres database — everything | All of it: agreement content, signer names and addresses, evidence, credentials as stored (hashed or encrypted) | us-west-2 |
| Railway | Runs the server processes: the API and the signing pages, the background worker that sends mail and texts, and the Ops console | Everything those processes handle, in memory while they handle it, and whatever they write to their logs (which are redacted by key name, src/lib/log.ts) | US (Virginia, us-east4) |
| Vercel | Serves signsealer.com, and passes the signing pages (/s, /f), the app (app.signsealer.com), certificate checks, webhooks and OAuth through to the API | The requests and pages as they pass through, over the TLS connection Vercel terminates -- a signer's page, their answers and signature as they are posted, a business's screens -- and Vercel's own request logs | Global edge, US origin |
| Sentry | Error reports from the API, the worker and Ops, when SENTRY_DSN is set (it is, in production) | The error's name, message and stack, the service and release, and fields redacted by key name -- never a header, a body or a cookie (src/lib/report.ts). An error's own message can occasionally include an email address | US |
| Resend | Sending signing links, reminders and completion copies; receiving bounces at signsealer.com | The recipient's address, the subject, and the message body — which contains the document's title and the signing link, but not the document's text | US |
| Stripe | Paying online: Checkout for prepaid credit, automatic reloads charged to the card it keeps (00402), the Customer Portal; subscriptions and invoices only for a business still on one from before 00402; the webhook back | The business's name, its contact email (or the member's, at Checkout), our tenant id and the amount (or, on an older subscription, the plan code) as metadata, and the card — which Stripe holds and we never see. Never a signer, a document or anything from the evidence store | US |
| OpenRouter, routing to Anthropic | Drafting help in the portal: a first draft of a template from a description, when a customer asks for one. Tidying: a template's wording re-set with headings and plainer sentences, when a customer presses the button. AI review before sending: things worth checking in a template's text, when a business has switched it on under Settings and asks for one. Reading a support answer aloud, when somebody presses play | For a draft: the business's name, the kind of document, the trade it picked in setup, where it operates, and what it typed into "what is this for". For a tidy or a review: the template's text as written — placeholders still in braces — with the business's name, the kind and where it operates. For a reading: only the answer's own words, which are a sentence we wrote from our own published documentation. Never a signer, never a filled-in blank, never a sent document, never anything from the evidence store | US |
| Telnyx | Sending text messages: signing links and reminders a signer asked for by text, and the six-digit sign-in codes to a verified mobile number once the keys are on the api service; receiving STOP and HELP | The phone number, and the message — a signing link, a reminder, or a code. Never a document's text | US |
| Google (Sign in with Google) | Another way to start a portal session, when configured and when the person chooses it | Google learns that this person signed in to SignSealer; we receive their Google subject, email, whether Google verified it, and their name. Nothing of ours goes to Google | US |
| Apple (Sign in with Apple) | The same, for Apple | Apple learns the same; we receive the Apple subject, the email (a relay address if they chose one), and the name once. Nothing of ours goes to Apple | US |
| Cloudflare (Turnstile) | Checking that the website's contact form is sent by a person (00403). Switched off until TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY are set on the api service and NEXT_PUBLIC_TURNSTILE_SITE_KEY on the site's build | The visitor's browser and IP address while the contact page is open, through its widget; the token that widget earns and the visitor's IP address when we ask whether it passed. Never the message, the name, the email or the phone number typed into the form | Global edge |
That is the whole list today.
What each one is given, precisely
Supabase holds the database. They are the processor for everything the product stores. Credentials are stored hashed or encrypted before they reach disk, and the encryption key is not in the database — so a Supabase employee with database access has ciphertext for webhook secrets and partner tokens, and plaintext for agreement content.
Resend is handed one message at a time: to, subject, text. The text of a signing-link email contains the signer's name, the sender's name, the document's *title*, and the link. It does not contain the document's body. A completion copy contains the title and the verification code.
The link is a bearer credential, which means it is in Resend's logs for their retention period. That is inherent to emailing a link and is the reason the token expires and is rotated on every reminder.
OpenRouter is handed one request per click of "Write a draft": a system prompt that is the same for everybody and a user message built from the six fields named in the table. src/portal/draft.ts is the whole of what builds it, and tests/draft.ts asserts the prompt holds no tenant id, user id, address or token. The draft that comes back is shown in the new-template form and is not stored by us until the customer saves it as a template; what is stored on every run is the model, the token counts and the cost (public.record_draft, 00190). OpenRouter's and Anthropic's own retention of prompts is governed by their terms; we send them nothing we would mind them keeping, which is the design rather than a hope. Switched off entirely when OPENROUTER_API_KEY is absent.
OpenRouter, for the review (src/portal/review.ts, 00202), is handed one request per click of "Review with AI first" on the send screen, and only for a business that has switched ai.review on under Settings — the engine refuses to record a review while it is off, so a screen cannot run one by mistake. The request is the template's text *as written*, placeholders still in braces ({{guest_name}}, not a name), with the business's name, the kind of document and where it operates. The blanks the person has just filled in on the send screen are not sent; neither is any signer. What comes back is read strictly: a finding whose quote is not in the text is dropped, a finding that is a verdict is dropped, and the rest are kept on document_reviews so that when the document is then sent the evidence package can say what was flagged and that it went out anyway. The model, tokens and cost are metered; the prompt and the raw answer are not kept. tests/review.ts asserts the prompt carries no id and no address.
OpenRouter, for tidying (src/portal/polish.ts), is handed one request per press of "Tidy up the wording" on a template screen. It carries the same things the review does and nothing more: the template's text as written, placeholders still in braces, with the business's name, the kind of document and where it operates. What comes back is checked before it is shown — every placeholder that went in must come back and none may be invented, and text that has collapsed to a summary is refused — and then it is shown in the form the person was already in, beside a button that restores their own words. It is never written to a template; a template changes only when somebody presses Save. The model, tokens and cost are metered through the same public.record_draft the drafter uses, so the two share one daily cap; the prompt and the raw answer are not kept. tests/templates.ts asserts the prompt's rules and the checks. Switched off entirely when OPENROUTER_API_KEY is absent.
OpenRouter, for reading an answer aloud (src/portal/voice.ts, 00357) is handed one request per press of play on a support answer. What it carries is the answer's own words and nothing else — not the question, not the business's name, not the person's. The answer is a sentence we wrote out of our own published documentation, so this is the one model call in the product that carries nothing of a customer's at all, and tests/portal.ts asserts both absences against the request body.
It cannot be handed anything else, and that is a property of the schema rather than a habit of the caller: public.speakable_answer takes the id of a recorded answer and returns that row's own text, so there is no parameter anywhere in the path for a sentence. A voice on this product can say exactly one class of thing — an answer that was already recorded, under the business hearing it, whose citations were already proved to point at real pages. The audio is returned to the browser and not stored; the request id, the model and the byte count go on the cost ledger. Switched off entirely, with no player on the page, when OPENROUTER_API_KEY is absent.
This was Hume's Octave, and the change is worth recording because it took Hume off the path of anything a customer does. Hume is still used -- by the Ops Studio, to voice SignSealer's own how-to videos from scripts we write -- and that carries no customer data at all (see below). Octave is $150 a million characters on the rung we would have been on — about six cents every time somebody pressed play on a four-sentence answer — against $7 a million for the same job here. What Hume is good at is knowing how a line should *feel*, which is worth paying for when a voice performs and worth nothing when it reads back "one reminder is sent, and only if they have not signed". The model and voice are OPENROUTER_TTS_MODEL and OPENROUTER_TTS_VOICE, so changing either is a variable rather than a new relationship for this document to describe.
Dictating a question is not a subprocessor of ours. The microphone button on the support screen uses the browser's own speech recognition (src/portal/dictate.ts), so no audio reaches us. It is worth being exact about where it does go, because the engines differ: Chrome sends the audio to Google to transcribe it and Safari can do it on the device. Neither sends it to us, and neither is acting for us — it is the person's own browser doing what they asked it to. The page says so beside the button.
Uploaded files are not a subprocessor at all. A .docx or a text file dropped on the templates screen is read in this process — src/portal/import.ts reads the zip and the paragraphs itself, with no library and no network — and what survives is the words, in the form, unsaved. The file is never written to disk, never stored, and never sent anywhere.
The wording of the four messages a signer reads is the business's own (00236) — the signing request by email and by text, the reminder, and the signed copy. That changes what is *said*, never who it goes to or what leaves the building: the same address, the same provider, the same signing link. Two sentences are not theirs to remove — "Reply STOP to opt out." and "Reply HELP for help." on a text are appended by the sender rather than carried in the template — and a signing request without its link is refused rather than sent.
Stripe is handed, per Checkout, the amount of credit being bought, a success and cancel URL, the business's name in the line's description, the tenant id and the amount as metadata on the session and its payment, and either the Stripe customer id already on file or the contact email. The card is kept for automatic reloads, each an off-session charge carrying the tenant id and the reload's id (src/worker/reloads.ts, 00402). Checkout for a plan's subscription, which carried a price id and the plan code, is no longer offered since plans gave way to the membership (00402). What comes back through /hooks/stripe is verified over the raw bytes with the endpoint's secret and reduced to ids, a status and a price before anything of ours reads it (src/billing/stripe.ts, app.apply_stripe_event). Switched off entirely when STRIPE_SECRET_KEY is absent.
On signsealer.com, and only after a visitor says yes
The marketing site can load Google Analytics, the Meta pixel and the LinkedIn Insight tag (apps/site/components/Consent.tsx), each only after the visitor accepts it in the cookie banner, and never on a signing page, a certificate check or the app. They receive that visitor's browser details and the site pages they read, under their own terms. They are not processors of customer data: no signer, no business's records and no document ever reaches them. What each sets is on /legal/cookies.
Connections a business chooses
A business can connect its own booking system or automation tool -- Guesty, Hostaway, Hospitable, FareHarbor, Zapier, Make, or its WordPress site. Data moves between SignSealer and that service because the business asked for it, under the business's own agreement with that service: the booking details that decide what to send, and the documents' status and links going back. They are the business's vendors rather than ours, and nothing moves to one until the business connects it.
What is not a subprocessor
- Timestamp authorities (Sectigo, DigiCert). A sealed PDF's timestamp is asked for with the SHA-256 of the file -- a fingerprint that reveals nothing of what the file says (
src/pdf/tsa.ts). - UptimeRobot fetches our public health pages to tell us when something is down. It sees no customer data.
- Hume and Google Cloud voice and render SignSealer's own videos in the Ops Studio (
src/ops/studio). They are handed scripts we wrote and screen recordings of demo accounts, never a customer's. - Apple App Store Connect and Expo build and publish the tablet app. They see our code, not customer data.
Changes
This page and the privacy policy carry the date they were last changed, and a new subprocessor that would see customer data is added here before it starts to. A notice period agreed with customers comes with a signed DPA, which is drafted and waiting on legal review; until then what we do as a processor says what applies. It is a prerequisite for any enterprise customer.

Ready when the next guest is.
Free for the first 25 agreements a month. No card to start.