Security
What protects the data you send us, what does not, and how to tell us about a hole. This page names the gaps as plainly as the controls, because a security page that only lists strengths is not information.
Last updated: July 27, 2026
Start with what we do not do
These are the answers a security questionnaire asks for first, and putting them last would be a way of hiding them.
- There is no encryption at rest. The database, the screenshot files and the backups sit unencrypted on the disks of the dedicated servers we rent in Beauharnois, Quebec, Canada. Passwords are hashed, which is neither encryption nor reversible, and that is the only cryptography applied to data at rest. If your risk assessment requires encryption at rest, SupportShot does not meet it today.
- There is no multi-factor authentication on SupportShot accounts. A password is the whole of it — this deployment offers no single sign-on and no social login either, so there is nowhere else to put a second factor. If your account matters, use a long unique password from a manager and keep it there.
- We hold no SOC 2, ISO 27001, or equivalent certification, and we have not commissioned an external penetration test. We will say so again rather than let a logo imply otherwise.
- There is no contractual uptime guarantee on any plan, and no bug bounty programme — we pay nothing for reports, and we say that up front rather than after somebody has spent a weekend.
- The widget cannot tell your secrets from your stack traces. It records the full page URL and your own console output verbatim. See the privacy policy, which sets out exactly where that leaves you.
What is actually in place
In transit
- Every connection to supportshot.com and to the ingest API runs over HTTPS. Cloudflare, Inc. is our authoritative DNS and nothing more — it does not sit in front of us, and there is no edge network between your visitors and our servers. Connections are made to the origin directly and TLS terminates there. We would rather say that than let a well-known name on the DNS imply a layer of protection that is not deployed.
- The widget submits to one origin and one only, so a report cannot be redirected by anything on the host page.
What the browser is told to enforce
- A Content Security Policy with a per-request nonce on the dashboard and on every page that handles a credential — sign-in, sign-up, password reset, address confirmation. There is no
'unsafe-inline'in itsscript-src: the inline scripts the framework emits carry that request’s nonce and anything else is refused. Beside it,base-uri 'none',object-src 'none',frame-ancestors 'none', and aform-actionlist naming the only three off-origin destinations a form on our pages may reach: Stripe Checkout, Stripe’s billing portal, and Google sign-in. - On every response, including the static files and the API:
X-Content-Type-Options: nosniff,Referrer-Policy: strict-origin-when-cross-origin,X-Frame-Options: DENY, aPermissions-Policythat names every powerful browser feature and turns it off rather than relying on a default, andCross-Origin-Opener-Policy. The framework banner,X-Powered-By, is switched off. /widget.jsis the one file we serve whose whole job is to be loaded by somebody else, and it says so withCross-Origin-Resource-Policy: cross-origin.- The policy does not run on this page, or on any of our marketing pages. A nonce has to be minted per request, and these pages are prerendered and served from a cache; giving them one would mean rendering each of them one visitor at a time. The dashboard and the credential pages are rendered per request anyway, so they pay nothing for it. These pages hold no session, no content anybody else supplied, and no form at all — every “Get started” here is a link to a page that does have the policy. They still carry every header in the second bullet; they carry no
script-src. We would rather write that down than let “we have a CSP” be read as the whole site.
Accounts and sessions
- Passwords are hashed with Argon2id and are not recoverable by us or anybody else. A sign-in attempt against an address with no account verifies against a dummy digest, so the response time does not reveal whether the address exists.
- Sessions are signed JWTs with a 30-day lifetime. Deleting a workspace signs the caller out immediately rather than leaving a token that outlives its own account row.
- Password-reset links expire two hours after they are sent; address-verification links expire after 48 hours. Sign-in and reset are rate limited per email address rather than per IP, so a botnet cannot lock somebody out of their own account by spraying it.
The ingest API, which is open to the internet by necessity
The widget runs on your site, in front of your visitors, so there is no credential it could hold that a visitor could not read. The public ss_pk_ key is an identifier, not a secret. What stands in for authentication is:
- Allowed origins. Every submission’s
Originheader is checked against the list on the project — exact scheme, host and port. A project with an empty list accepts reports and marks them unverified in your inbox rather than silently trusting them. - Two rate limits. Ten submissions per minute per project, and sixty per minute per source IP address applied before the project is resolved — the second one bounds how much parsing an anonymous caller can make our servers do.
- A hard body ceiling on the upload, with every field inside it bounded, so no single request can consume unbounded memory.
- A reserved namespace in the stored context: the server’s own record of how a report got in is written under a key the client cannot claim, over whatever arrived there.
- An unknown project key and a disabled project return the same answer, so the endpoint cannot be used to test whether a key exists.
Screenshots
- Stored on the server’s own disk under a per-project directory, not in a third-party object store.
- In the dashboard they are served through authenticated routes. In notification email and in project exports they are served through links signed with HMAC-SHA256 over the ticket, the file and an expiry, verified in constant time. The signature resolves through the ticket row, so deleting a ticket kills every image link ever emailed for it, in every mailbox, immediately — and the link now also dies on its own, 180 days after it was signed.
- An expired link answers 410 with prose rather than a blank refusal, and names the two routes that have no expiry to run out: the report in the dashboard, which serves the same image to anyone signed in to the workspace, and a fresh export, whose links are signed at the moment the file is downloaded. Links signed before the expiry existed are refused rather than grandfathered — a compatibility window for a credential that carries no date is a permanent credential with a longer story. The residual is worth naming: a recipient with no SupportShot account, holding a link more than 180 days old, has to ask somebody who has one.
- Path handling refuses traversal and reserved suffixes rather than normalising them.
Tenancy and deletion
- Every dashboard query is scoped to the caller’s workspace. The workspace erasure runs in one transaction in the only order the foreign keys allow, and the completeness of that order is asserted from
information_schemain the test suite — so a new table that references a project, ticket, user or org fails a test rather than quietly surviving somebody’s erasure request. - Deleting is the only irreversible action in the product and it is gated: a workspace erasure is owner-only and needs the workspace name typed.
Logging
- We keep no per-request access logs. Access logging is switched off at the proxy that terminates every request, so there is no per-visit record of IP address, URL, user agent or status anywhere in our systems. Application event logs rotate on a fixed-size basis, typically within days and never beyond 30.
- One rate-limit counter holds an IP-derived key: the sixty per minute per source IP address on the ingest API. It holds the address a submission arrived from, for the length of its window, and is swept within hours — and it is the only place we hold a visitor IP. In particular the settings request the widget makes on every page load is not counted and not recorded: that endpoint reads no forwarding header and writes nothing at all.
Backups
Backups exist so a fault does not become a data loss. One is taken every day — a database dump and an archive of the screenshot files, stamped as the pair they are — and the job then restores that dump into a scratch database and compares its row counts against the live ones, because an archive nobody has restored is not a backup. They are kept on a volume of their own, separate from the database and from the uploads, under the same access control as production, and no backup is kept for more than 14 days. That volume is on the same machine as the data it holds: a copy on the same host survives a mistake and not a fire, and we would rather say so than imply a second site we have not set up. They are never opened to answer a support request or a data request, and after any restore we re-run every erasure that happened since the backup was taken, so a restore cannot resurrect a workspace somebody asked us to destroy. They are unencrypted, like everything else at rest.
The widget, on your page
- It mounts in a shadow root attached to the document element, adds one global, and never throws into your page. If anything inside it fails it disables itself and restores
console,fetchandXMLHttpRequestexactly as it found them. - It sets no cookies and writes nothing to
localStorageorsessionStorageon your visitors’ devices. - It is a single self-contained file with no runtime dependencies and no npm package, so installing it adds nothing to your supply chain but one script tag. The docs publish the exact Content Security Policy it needs, including the hash of the inline queue stub, so you never need
'unsafe-inline'for it.
Who else touches the data
All of it is at rest with OVH Hosting Inc. (OVHcloud) in Beauharnois, Quebec, Canada. The full list, with what each one processes and where, is the subprocessor table, and you get 30 days’ notice and a right to object before that list changes.
If something goes wrong
If we suffer a breach affecting your data we will tell you without undue delay, and in time for you to meet your own 72-hour notification deadline where we are your processor. We will say what we know at the time rather than wait until we know everything, and follow up as we learn more. The obligation is written down, in operative form, in clause 10 of the data processing agreement.
Reporting a vulnerability
Email support@supportshot.com. There is no form, no portal, and no NDA to sign first. The same address is published at /.well-known/security.txt.
What we promise
- We acknowledge a report within 3 business days and tell you what we think of it within 10.
- We will not pursue legal action, and will not support anybody else pursuing it, over a good-faith report that follows the rules below.
- We will tell you when it is fixed, and credit you by name on request.
- There is no monetary reward. We are a small company and we would rather say that in advance than after you have done the work.
What we ask
- Test against your own account, your own projects, and your own project keys. Do not access, modify, or store another customer’s data — if a bug gives you access to it, stop, and tell us what you saw rather than how much of it you could get.
- No denial of service, no load or stress testing, no spam, and no social engineering of our people or our providers.
- Give us a reasonable chance to fix it before publishing. Ninety days is our default and we will not ask for longer without explaining why.
- Send enough for us to reproduce it: the request, the response, the steps, and what you expected instead.
Out of scope
- Findings that restate something on this page: the absence of encryption at rest, the absence of MFA, the absence of a certification.
- Version disclosure, and reports whose whole content is the output of a scanner. A header finding is in scope if you can say what it would have stopped here — the ones we ship are listed above, and a gap in them is worth an email.
- That the public
ss_pk_project key is readable in a customer’s HTML. It is designed to be, and the allowed-origins list is what constrains it. - Anything requiring a compromised device, a compromised email account, or a browser out of support.
Contact
Security reports and questions: support@supportshot.com. In writing: 2662415 Ontario Inc., 408 Dundas Street South, Unit 101, Cambridge, Ontario N1R 8H7, Canada.