Security & vulnerability disclosure
Last updated: May 24, 2026
What we do to protect your data, what to do if something goes wrong, and how to report a vulnerability without ending up in a lawyer's inbox.
Encryption
- TLS 1.2+ enforced on every connection. HSTS preload (2-year max-age, includeSubDomains).
- AES-256 at rest in Supabase (Postgres) and Stripe.
- Secrets (Stripe webhook secrets, Supabase service-role key, Anthropic gateway tokens) are stored as encrypted Vercel environment variables and never reach the browser.
Access controls
- Row-Level Security (RLS) enabled on every table in the public schema. Owner-scoped policies enforce per-account isolation at the database layer — verified by a cross-user isolation test in the repo.
- Authentication via Supabase Auth (magic link + Google OAuth). PKCE flow for OAuth. Sessions are HTTP-only Secure cookies.
- Webhook signature verification on all third-party callbacks (Stripe).
- Admin operations use a separate service-role client that bypasses RLS only on intentional code paths (e.g. the Stripe webhook); never reachable from end-user input.
Network and application security
- Content-Security-Policy on every response — script/style/connect/frame sources explicitly allowlisted; object-src 'none'; frame-ancestors 'none' except on /embed/iframe.
- X-Frame-Options DENY everywhere except the iframe-embed page (intentionally embeddable).
- X-Content-Type-Options: nosniff site-wide.
- Permissions-Policy disabling 16 unused browser features (camera, microphone, geolocation, USB, magnetometer, etc.).
- Cross-Origin-Opener-Policy: same-origin-allow-popups for Spectre isolation while preserving OAuth.
- Cross-Origin-Resource-Policy: cross-origin on embed assets; same-site implicit elsewhere.
- CORS limited to the embed-flow endpoints; auth, chat, billing, and newsletter endpoints reject cross-origin POSTs with 403 (CSRF defense).
- Open-redirect guard on every user-supplied `next` parameter — rejects protocol-relative URLs, backslash tricks, CRLF/control-character injection, and non-path schemes.
- Distributed rate limiting on every public POST endpoint, backed by Postgres (Supabase) so caps are global across all serverless instances, with a safe in-process fallback. Current caps: chat 15/min/IP, newsletter 5/5min/IP, testimonial submission 5/hour/project/IP + 30/hour/IP global.
- Honeypot fields on every public form (newsletter, testimonial submission).
- Privacy-by-design: testimonial author emails are stored in a column the public API physically cannot read; the row is exposed only via a `testimonials_public` view that omits the column and is now `security_invoker = true` (honors caller RLS).
- Stripe webhook idempotency: every event ID recorded in `processed_stripe_events`; duplicate deliveries short-circuit with 200 without re-processing.
Defenses against AI-bill exhaustion
The chatbot calls Anthropic Claude via Vercel AI Gateway, which would be a tempting target for cost-exhaustion abuse without guardrails. Five overlapping defenses:
- Origin check — the endpoint rejects POSTs whose Origin header doesn't match our domain. Stops curl loops from third-party sites instantly.
- Per-IP rate limit — 15 messages per minute, distributed via the shared Postgres backend (consistent across Vercel Function instances).
- Message-count cap — only the last 20 messages of conversation history are forwarded; an attacker can't grow context by appending unbounded history.
- Output-token cap — every response is hard-limited to 500 tokens via `maxOutputTokens`.
- Model choice — Claude Haiku, the cheapest tier (~$1/M input, ~$5/M output).
Audit log
An append-only audit_log table records mutations on testimonials, widgets, projects, and subscriptions plus security-relevant events (sign-in, rate-limit triggers, suspicious activity). Owners can read their own events via the dashboard; writes go through a SECURITY DEFINER helper that only the service-role client can invoke. Schema and policy live in migration 0005_security_hardening.sql.
Subprocessors and data location
The full list of every third party we share data with, what data they touch, and where they're located is at /legal/subprocessors. Cross-border transfers are documented with the relevant mechanism (EU-US DPF where the recipient is certified; Standard Contractual Clauses otherwise).
Incident response
If we detect or are notified of a security incident affecting customer data:
- T+0 — Confirm scope; rotate any compromised credentials; revoke affected sessions.
- T+1h target — Internal incident channel opened; on-call engineer triaging.
- T+24h target — Containment + initial root cause identified.
- T+72h required (GDPR Art. 33) — Notify supervisory authority if the breach is likely to result in a risk to data subjects' rights and freedoms.
- Without undue delay (GDPR Art. 34) — Notify affected users directly when the risk is high.
- Post-incident — Public post-mortem published at /changelog. Remediation tracked through completion.
We publish post-mortems for any incident that affected customer data, regardless of whether legal notification was required.
Backups and recovery
- Supabase performs automated daily backups with 7-day retention on free / 14-day on paid Supabase plans.
- Stripe is the source of truth for subscription state; the local mirror is rebuildable from webhooks.
- Recovery point objective (RPO): 24 hours. Recovery time objective (RTO): 4 hours.
Compliance posture
- GDPR — controller/processor obligations documented; SCCs + DPF as transfer mechanisms; DPA available on request (see /legal/dpa).
- CCPA / CPRA — California rights honored; GPC signal respected.
- CAN-SPAM — postal address and one-click unsubscribe on all marketing email; no marketing content in transactional email.
- PCI DSS — out of scope: card data is handled entirely by Stripe (PCI Service Provider Level 1). We never see or store PANs.
- SOC 2 — not currently audited. On the roadmap once revenue justifies. Underlying infrastructure (Supabase, Stripe, Vercel) is SOC 2 Type II.
Dependency security
- pnpm audit on every install; transitive vulnerabilities tracked and pinned via overrides in pnpm-workspace.yaml when upstream hasn't shipped a patched parent.
- GitHub Dependabot enabled for security + version updates (weekly).
- Production deploys block when a known high or critical advisory affects a runtime dependency.
Machine-readable security contact
A /.well-known/security.txt file (RFC 9116) tells automated scanners and researchers exactly where to report. Re-generated every deploy so the expiration date stays under one year.
Vulnerability disclosure
We welcome reports from security researchers. Please email security@plauditly.app with:
- A clear description of the vulnerability and where it lives (URL, parameter, payload).
- Steps to reproduce, including any account credentials you used. Do not test against accounts that are not yours.
- Your assessment of impact.
- Whether you'd like to be credited publicly (and the name to use).
We aim to acknowledge reports within 3 business days and provide a remediation timeline within 10 business days. We will not pursue legal action against researchers who:
- Make a good-faith effort to avoid privacy violations, data destruction, and service degradation.
- Don't access more data than necessary to demonstrate the vulnerability.
- Don't publicly disclose before we've had a reasonable opportunity to fix.
- Don't extort, demand payment for a fix beyond a published bounty (we currently have none), or breach applicable law.
Out of scope for disclosure: DoS / volumetric attacks, social engineering of staff, physical attacks, and findings that require full admin access to a user account you don't own.
Security contact
Security disclosures: security@plauditly.app. PGP key available on request. For non-security questions, use hello@plauditly.app.