Security
Security designed for evidence that has to hold up in court.
DSP Watch stores the audit log a §512(c)(3) takedown depends on. Tenant isolation, signer attestation, evidence integrity and edge controls are built so a hostile re-upload, a leaked link or a tampered row can be detected and disproven. This page documents the controls; for our customer-facing trust posture see /trust.
Tenant isolation
How is workspace data kept separate?
Every row that belongs to a workspace is gated by Postgres row-level security. The isolation boundary lives in the database, not in the API, and is the same boundary auditors can verify with a SQL query.
-
Postgres row-level security
Every workspace-owned row carries a workspace_id column with a RLS policy that joins to the authenticated user's workspace membership. The check runs at the database, not the application — even a runaway query in the API layer cannot cross workspaces.
-
Signed JWT workspace binding
Worker-issued JWTs carry the workspace_id claim. Supabase reads the claim through PostgREST's auth.jwt() and feeds it to RLS. Switching workspaces means re-authenticating, not flipping a cookie.
-
Per-workspace storage prefixes
Evidence PDFs in Cloudflare R2 are keyed under workspaces/<workspace_id>/<sha256>.pdf. Listing or reading another workspace's prefix requires a signed URL the issuing workspace cannot mint.
Audit log
How is the audit log made tamper-evident?
Each audit_log row is hashed with SHA-256 over its content plus the previous row's hash. The chain is verifiable end to end and replayable from a single SQL query.
-
SHA-256 chained over prev_hash
Each audit_log row carries prev_hash (the previous row's content hash) and content_hash (SHA-256 over actor, action, target, timestamp, payload and prev_hash). Tampering with any historical row breaks every subsequent hash.
-
Verifiable end to end
The chain can be replayed from genesis to head with one SQL query. The verification job runs nightly and alerts on first mismatch. Auditors can pull the full chain export as NDJSON on request.
-
Immutable in practice
Append-only by RLS — no UPDATE or DELETE policy exists on audit_log for any role except the dedicated retention service, which only ever deletes whole epochs older than the retention window.
Evidence integrity
How is evidence pinned to a takedown?
Evidence PDFs are content-addressable by SHA-256. Identical bundles collapse to one object; any byte changed produces a new address. The digest is the link between the PDF, the action row and the audit log entry that records its submission.
-
Content-addressable by SHA-256
Every evidence PDF is stored at its own SHA-256 digest. Two identical evidence bundles deduplicate naturally; any byte changed produces a new address. The digest is bound to the takedown action row in Postgres.
-
Reproducible from inputs
PDFs are generated deterministically from the finding, the §512(c)(3) template version and the signer attestation. Re-running the generator with the same inputs yields the same SHA-256, which we assert in CI.
-
Tamper-evident transport
When a recipient host downloads the PDF, the SHA-256 ships in the audit log and in the action.evidence_uploaded webhook payload. A hash mismatch on download invalidates the action.
Signer attestation
How do we prove a human signed the takedown?
A takedown is signed by a person attesting under penalty of perjury. We bind that attestation to a single action with an MFA-fresh JWT that cannot be replayed.
-
MFA-fresh attestation
Filing a takedown requires a signer attestation JWT minted no more than 5 minutes after the signer re-authenticated with MFA. The freshness is a JWT claim, validated by the Worker at submit time.
-
Per-action JWT, not per-session
The attestation JWT is single-use — bound by jti to one action_id. Replaying it against a second action is rejected by the Worker's jti store.
-
Signed §512(c)(3) statements
Both statutory good-faith statements (§512(c)(3)(A)(v) and (vi)) are signed by the attesting party and reproduced verbatim in the evidence PDF, with the signer's name, role and the timestamp of MFA freshness.
Edge and transport
What protects traffic between Worker, Fly and R2?
Every public ingress is fronted by Cloudflare. Internal traffic stays on Fly's private flycast network. Object access uses scoped, short-lived signed URLs. Inter-service calls are signed JWTs with replay protection.
-
Cloudflare WAF on every route
The marketing site, app and API all sit behind Cloudflare's WAF. OWASP Core Rule Set is on for high-severity rules; bot management blocks credential-stuffing patterns before they reach the Worker.
-
Fly.io private flycast network
The Fly app exposes its internal services only over the .flycast private network. Public ingress lives behind a single TLS edge that requires a Worker-issued JWT before routing to a backend.
-
R2 with workspace + IP-bound signed URLs
Evidence downloads use R2 presigned URLs scoped to the workspace prefix and the requesting IP. URL lifetime is 5 minutes. A leaked link from a different egress IP fails to resolve.
-
Worker↔Fly HS256 JWT with jti replay protection
Every Worker→Fly call carries an HS256 JWT signed with a 256-bit shared secret. The jti is single-use within a 60-second clock-skew window; the Fly side rejects replays from a Redis-backed jti set.
Sub-processors
Who processes data on our behalf?
These vendors process customer data as part of normal operation. We notify customers in writing at least 30 days before adding a new sub-processor that handles workspace content.
| Vendor | Purpose | Region |
|---|---|---|
| Cloudflare, Inc. | Edge CDN, WAF, Workers runtime, R2 object storage, Pages hosting. | Global edge; metadata in US/EU. |
| Supabase, Inc. | Managed Postgres (primary data store), auth, RLS, PostgREST. | AP-Northeast-1 (Tokyo). |
| Fly.io, Inc. | Compute for evidence rendering, DSP scanners and adapter workers. | arn (Stockholm) primary; .flycast private network. |
| Stripe, Inc. | Subscription billing, Checkout, customer portal, webhooks. | US; PCI DSS Level 1. |
| Resend, Inc. | Transactional email — verification, attestation, counter-notice warnings. | US. |
Vulnerability disclosure
Found something? Tell us.
We welcome responsible disclosure from security researchers. Email security@dspwatch.com with a description of the issue, reproduction steps and any proof-of-concept. We acknowledge reports within 2 business days and aim to triage within 5. Please do not test against live customer workspaces or run automated scanners that generate sustained load against our edge — reach out first and we will help you reproduce safely.
Reporting in scope
dspwatch.com, app.dspwatch.com, dsp-watch-api.jeeb.workers.dev and any subdomain reachable from a published DSP Watch link. Out of scope: third-party DSPs we file takedowns with.