docs(audits): GDPR compliance audit 2026-07 — 35 findings, ROPA starter, P0-P2 roadmap - #1386
docs(audits): GDPR compliance audit 2026-07 — 35 findings, ROPA starter, P0-P2 roadmap#1386kristofferremback wants to merge 1 commit into
Conversation
Full audit of rev 3895bd0 against Regulation (EU) 2016/679 (consolidated text read in full). 35 findings (G-01..G-35) with path:line citations, ROPA-starter inventory, and a P0/P1/P2 remediation roadmap. Three structural Criticals: no erasure capability, no transparency layer, uncontrolled US egress via the AI chain. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughSummary by CodeRabbit
WalkthroughChangesGDPR audit
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/audits/gdpr-audit-2026-07.md`:
- Line 14: Update the opening GDPR finding to describe the gap as no complete,
user-triggerable erasure capability across the data lifecycle, replacing the
absolute statement that no erasure capability exists. Preserve the detailed
findings about soft deletion, unused hard-delete paths, attachments, and derived
AI memory.
- Line 22: Rephrase the sentence beginning “Because Threa is pre-GA” to state
that the waitlist stage reduces current scale and risk, while GDPR obligations
already apply to qualifying personal-data processing within the relevant
material and territorial scope. Remove the implication that obligations begin
only when an external EU user joins a workspace, and keep the existing
remediation-roadmap reference.
- Around line 125-129: Reconcile the Article 22 and Article 35 assessments in
the G-21 and G-22 entries: retain the DPIA recommendation, but revise the
Article 35 rationale to rely on the likely-high-risk analysis under Article
35(1) and the documented WP248 criteria unless a qualifying legal or similarly
significant decision is identified. Ensure the report no longer presents Article
35(3)(a) as directly applicable while stating memo creation has no Article
22-level effect.
- Around line 102-109: Update the G-14, G-15, and G-16 findings in the audit to
distinguish provider jurisdiction from physical processing location. Replace
assertions of confirmed US processing with wording that identifies third-country
transfer risk and states that the actual processing region is unverified, while
preserving the documented endpoint and data-egress concerns. Align the routing
description with docs/system-overview.md by identifying the regional backend as
the OpenRouter/Langfuse caller without claiming that this proves US physical
processing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 60c3b56d-1eaf-4a49-a5b4-48af7419217d
📒 Files selected for processing (1)
docs/audits/gdpr-audit-2026-07.md
📜 Review details
🧰 Additional context used
📓 Path-based instructions (2)
**/*
📄 CodeRabbit inference engine (CLAUDE.md)
**/*: Keep replies and PR descriptions concise, factual, and free of filler.
Use Bun commands instead of Node.js, npm, Jest, Vitest, ts-node, webpack, or esbuild equivalents.
Files:
docs/audits/gdpr-audit-2026-07.md
**
⚙️ CodeRabbit configuration file
**: Architecture, invariants, and the full app inventory live in CLAUDE.md and
docs/system-overview.md. Treat those as the source of truth.What NOT to flag:
- Pre-existing issues not introduced by this PR
- Issues that TypeScript compilation or ESLint would catch (types, imports, lint)
- Stylistic preferences without a concrete rule violation in CLAUDE.md
- Theoretical risks or hypothetical edge cases without evidence of exploitability
- General best-practice suggestions that don't map to a specific project rule
Security calibration:
- React JSX is safe from XSS unless dangerouslySetInnerHTML is used
- ULIDs/UUIDs are cryptographically unguessable — do not flag as enumeration risks
- Environment variables are trusted — do not flag as hardcoded secrets
- DoS, rate limiting, log spoofing, regex complexity, missing audit logs, and
outdated dependency warnings are out of scopePlan adherence:
The implementation plan is in the PR description, inside the collapsible
"📋 Full implementation plan" details block (plans are not committed to the repo).
If that block exists, check that PR changes align with the plan.
Flag missing corresponding changes: API change without frontend/backoffice update,
type change without usage update, schema change without migration.
Files:
docs/audits/gdpr-audit-2026-07.md
🧠 Learnings (1)
📚 Learning: 2026-05-30T19:08:44.359Z
Learnt from: kristofferremback
Repo: threahq/threa PR: 706
File: docs/features/README.md:29-33
Timestamp: 2026-05-30T19:08:44.359Z
Learning: In threahq/threa Markdown documentation, allow bare fenced code blocks (no language/info tag) when they’re intentionally used to display file/directory tree listings. Do not flag these blocks with markdownlint rule MD040 and do not recommend adding a `text` (or any other) language identifier as an info tag.
Applied to files:
docs/audits/gdpr-audit-2026-07.md
🪛 LanguageTool
docs/audits/gdpr-audit-2026-07.md
[grammar] ~30-~30: Ensure spelling is correct
Context: ... individual's own use, but never covers Threa as provider. - Current stage: one acti...
(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)
[grammar] ~84-~84: Use a hyphen to join words.
Context: ...ures a ToS/privacy display on the WorkOS hosted screen (`apps/control-plane/src/f...
(QB_NEW_EN_HYPHEN)
[style] ~137-~137: ‘out of reach’ might be wordy. Consider a shorter alternative.
Context: ... push rows, CP tables, and logs are all out of reach. No consolidated access surface exists....
(EN_WORDINESS_PREMIUM_OUT_OF_REACH)
[uncategorized] ~154-~154: The official name of this software platform is spelled with a capital “H”.
Context: ...ecret scanning, or Dependabot anywhere (.github/ grep-verified for snyk/codeql/dependa...
(GITHUB)
[grammar] ~154-~154: Use a hyphen to join words.
Context: ...dependabot.yml). For an Art. 32 "state of the art" argument this is the weakest po...
(QB_NEW_EN_HYPHEN)
[grammar] ~154-~154: Use a hyphen to join words.
Context: ...abot.yml`). For an Art. 32 "state of the art" argument this is the weakest point....
(QB_NEW_EN_HYPHEN)
[uncategorized] ~157-~157: The official name of this software platform is spelled with a capital “H”.
Context: ... (scripts/check-migrations.ts:95-122; .github/workflows/ci.yml:34-41). The db-read-p...
(GITHUB)
[uncategorized] ~179-~179: The official name of this software platform is spelled with a capital “H”.
Context: ...users — email, name, pronouns, phone, github, timezone, status; trigram-indexed name...
(GITHUB)
🪛 markdownlint-cli2 (0.23.0)
docs/audits/gdpr-audit-2026-07.md
[warning] 194-194: Ordered list item prefix
Expected: 1; Actual: 5; Style: 1/2/3
(MD029, ol-prefix)
[warning] 195-195: Ordered list item prefix
Expected: 2; Actual: 6; Style: 1/2/3
(MD029, ol-prefix)
[warning] 196-196: Ordered list item prefix
Expected: 3; Actual: 7; Style: 1/2/3
(MD029, ol-prefix)
[warning] 197-197: Ordered list item prefix
Expected: 4; Actual: 8; Style: 1/2/3
(MD029, ol-prefix)
[warning] 201-201: Ordered list item prefix
Expected: 1; Actual: 9; Style: 1/1/1
(MD029, ol-prefix)
|
|
||
| Threa is **not currently in a defensible GDPR position for operating with external EU users**, but the gaps are concentrated and the foundations underneath are unusually good. Three structural problems dominate; almost everything else hangs off them: | ||
|
|
||
| 1. **There is no erasure capability anywhere in the data lifecycle** (Art. 17, 5(1)(e)). No account deletion, no workspace deletion; message "deletion" is a soft flag whose content keeps being stored _and served_; attachments are never removed from S3; derived AI memory ignores source deletion. The event-sourced architecture has no redaction path. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
Describe the gap as missing end-to-end erasure.
The document later identifies soft deletion and an unused hard-delete path. Replace “There is no erasure capability anywhere” with “There is no complete, user-triggerable erasure capability across the data lifecycle” to avoid contradicting the detailed findings.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/audits/gdpr-audit-2026-07.md` at line 14, Update the opening GDPR
finding to describe the gap as no complete, user-triggerable erasure capability
across the data lifecycle, replacing the absolute statement that no erasure
capability exists. Preserve the detailed findings about soft deletion, unused
hard-delete paths, attachments, and derived AI memory.
|
|
||
| **What is genuinely strong** (and worth preserving as compliance evidence): all first-party infrastructure is physically EU (Railway europe-west4, S3 eu-north-1); there is **zero** third-party analytics, error tracking, or ad tech on any surface; cookies are strictly-necessary only (no banner needed); access control is centralized, fail-closed, and covered by real CI tests; API keys are hashed with timing-safe comparison; push payloads are RFC 8291-encrypted; voice audio and transcripts are never persisted server-side; E2E-encrypted scratchpads exist and are verifiably excluded from all AI/memory processing; evals never touch production data. | ||
|
|
||
| Because Threa is pre-GA (waitlist stage, essentially personal use today), none of this is an emergency **yet** — but every gap above becomes a live legal obligation the moment the first external EU user joins a workspace. The remediation roadmap in §6 is ordered accordingly. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not defer GDPR applicability until workspace onboarding.
The report already documents current waitlist email collection and other personal-data processing. Rephrase this to say the pre-GA stage lowers scale and present risk; it does not mean GDPR obligations begin only when the first external EU user joins a workspace. GDPR applies to qualifying processing within its material and territorial scope. (eur-lex.europa.eu)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/audits/gdpr-audit-2026-07.md` at line 22, Rephrase the sentence
beginning “Because Threa is pre-GA” to state that the waitlist stage reduces
current scale and risk, while GDPR obligations already apply to qualifying
personal-data processing within the relevant material and territorial scope.
Remove the implication that obligations begin only when an external EU user
joins a workspace, and keep the existing remediation-roadmap reference.
Source: MCP tools
| **G-14 · Critical · All content exits to OpenRouter (US) with no safeguards expressible in code.** | ||
| The endpoint is hardcoded (`packages/agent-runtime/src/ai/ai.ts:29`); there is no provider pinning, no EU routing, no ZDR/`data_collection` opt-out anywhere (grep-verified incl. models.yaml). Because GAM classifies/memorizes nearly every conversation, embeds **every message ≥10 chars** (`apps/backend/src/features/memos/embedding-worker.ts:36-46`), and enriches with attachment text and base64 images, this is a near-total egress of workspace content — not an opt-in feature. `docs/model-reference.md` notes an EU-pinned model exists, but no code pins it. Required: DPAs + SCC/transfer assessment with OpenRouter (and transitively Anthropic/OpenAI/Google), or EU-pinned direct endpoints. | ||
|
|
||
| **G-15 · High · Langfuse receives unmasked prompts, completions, and identity.** | ||
| `experimental_telemetry` is enabled with no `recordInputs/recordOutputs: false` and no mask function (`packages/agent-runtime/src/ai/ai.ts:611-636`; `apps/backend/src/lib/langfuse/langfuse.ts:42-53`); traces carry `userId`/`sessionId` (`langfuse.ts:81-92`); the agent observer writes tool I/O and final content onto spans (`packages/agent-runtime/src/runtime/otel-observer.ts:47-98`). Production `LANGFUSE_BASE_URL` is **unverified** (default is localhost); if it points at a hosted instance this is a second full-content processor with no retention control. | ||
|
|
||
| **G-16 · High · Voice audio streams to hardcoded US endpoints.** | ||
| `wss://api.elevenlabs.io/...` (`apps/backend/src/features/voice-transcription/transcription/realtime-elevenlabs.ts:16`) and `wss://api.deepgram.com/...` (`realtime-deepgram.ts:16`), plus user/workspace steering vocabulary (often names, `realtime-gateway.ts:245-250`). No EU endpoint option. Dictation polish also sends surrounding composer draft context to the LLM (`apps/frontend/src/hooks/use-voice-dictation.ts:720-726`). |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Separate provider jurisdiction from physical processing location.
These findings assert US processing, while §7 marks the physical regions of OpenRouter, ElevenLabs, and Deepgram as unverified. Hostnames and provider jurisdiction do not establish the physical processing region. Use wording such as “third-country transfer risk; processing region unverified” until contracts or deployment evidence confirm the location.
As per path instructions, docs/system-overview.md is the source of truth for routing and identifies the regional backend as the OpenRouter/Langfuse caller, but it does not establish US physical processing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/audits/gdpr-audit-2026-07.md` around lines 102 - 109, Update the G-14,
G-15, and G-16 findings in the audit to distinguish provider jurisdiction from
physical processing location. Replace assertions of confirmed US processing with
wording that identifies third-country transfer risk and states that the actual
processing region is unverified, while preserving the documented endpoint and
data-egress concerns. Align the routing description with docs/system-overview.md
by identifying the regional backend as the OpenRouter/Langfuse caller without
claiming that this proves US physical processing.
Source: Path instructions
| **G-21 · Medium · Art. 22 — likely not triggered, but document it.** | ||
| No automation makes decisions with legal or similarly significant effects on humans: no AI moderation, suspension, access revocation, or delivery gating (grep-verified); persona auto-actions have no destructive tools and are timeline-visible; suggestions are pull-only; quota logic affects bots, not humans; rate limits are 429-only. Memo creation is the strongest candidate and still falls short of "legal or similarly significant effect" — but this assessment should be recorded, and human-contest paths (edit/archive/off-switch) kept intact as they are today. | ||
|
|
||
| **G-22 · High · No DPIA exists, and one is warranted (Art. 35).** | ||
| Systematic large-scale evaluation of personal aspects with innovative technology (automatic memory extraction, profiling tiers, workspace-wide knowledge synthesis) matches Art. 35(3)(a) and WP248 criteria. No DPIA, LIA, or processing-risk document exists in the repo. |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Reconcile the Article 22 and Article 35(3)(a) assessments.
If memo creation does not produce legal or similarly significant effects, the report should not state that it matches Article 35(3)(a) without further explanation. Keep the DPIA recommendation, but ground it in Article 35(1)’s likely-high-risk analysis and the documented WP248 criteria, or identify the qualifying significant decision. (eur-lex.europa.eu)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/audits/gdpr-audit-2026-07.md` around lines 125 - 129, Reconcile the
Article 22 and Article 35 assessments in the G-21 and G-22 entries: retain the
DPIA recommendation, but revise the Article 35 rationale to rely on the
likely-high-risk analysis under Article 35(1) and the documented WP248 criteria
unless a qualifying legal or similarly significant decision is identified.
Ensure the report no longer presents Article 35(3)(a) as directly applicable
while stating memo creation has no Article 22-level effect.
Source: MCP tools
Problem
Threa has no recorded assessment of its GDPR posture. With the waitlist opening toward external EU users, there was no inventory of what personal data exists where, no view of which Articles are unmet, and no ordered plan for closing the gaps.
Solution
A full audit of rev
3895bd0bagainst Regulation (EU) 2016/679 (the consolidated EUR-Lex text, read in full, Articles 1–99), committed asdocs/audits/gdpr-audit-2026-07.mdalongside the existing e2ee-enclave audit. Method: six parallel code audits (data inventory/retention, data-subject rights, processors/transfers, security, transparency/consent, AI layer/profiling) synthesized into one report.Contents:
path:lineat the audited revision; deployment-console facts explicitly marked unverified (§7 lists them).event-service.ts:1894-1900; message attachments never leave S3); no transparency layer (G-08 — no policy/ToS/notices on any surface); uncontrolled US egress via the AI chain (G-14..G-17 — OpenRouter hardcoded with no EU pinning, unmasked Langfuse telemetry,S3_REGIONsilently defaulting tous-east-1).Files changed
docs/audits/gdpr-audit-2026-07.mdTest plan
path:linecitations produced by direct code inspection at3895bd0bduring the audit; spot-checkable via the G-numbers🤖 PR by Claude Code
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.