Skip to content

docs(audits): GDPR compliance audit 2026-07 — 35 findings, ROPA starter, P0-P2 roadmap - #1386

Open
kristofferremback wants to merge 1 commit into
mainfrom
audit/gdpr-audit
Open

docs(audits): GDPR compliance audit 2026-07 — 35 findings, ROPA starter, P0-P2 roadmap#1386
kristofferremback wants to merge 1 commit into
mainfrom
audit/gdpr-audit

Conversation

@kristofferremback

@kristofferremback kristofferremback commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

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 3895bd0b against Regulation (EU) 2016/679 (the consolidated EUR-Lex text, read in full, Articles 1–99), committed as docs/audits/gdpr-audit-2026-07.md alongside 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:

  • 35 findings (G-01..G-35), severity-rated Critical/High/Medium/Low, every claim cited path:line at the audited revision; deployment-console facts explicitly marked unverified (§7 lists them).
  • Three structural Criticals: no erasure capability anywhere in the data lifecycle (G-01..G-04 — deleted message content is still served on bootstrap, 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_REGION silently defaulting to us-east-1).
  • What's already strong, recorded as compliance evidence (§3): EU-only first-party infra, zero trackers, strictly-necessary cookies, fail-closed access control with CI tests, E2E streams excluded from all AI processing.
  • ROPA-starter inventory (§5) and a P0/P1/P2 remediation roadmap (§6) keyed to the trigger point "first external EU user joins a workspace".
  • An Art. 22 assessment (not triggered — no automation makes legal/similarly-significant decisions about humans) and a DPIA recommendation for GAM.

Files changed

File Change
docs/audits/gdpr-audit-2026-07.md New — the audit report (207 lines)

Test plan

  • Doc-only change; pre-commit hooks ran full repo lint + typecheck across all workspaces — clean
  • All path:line citations produced by direct code inspection at 3895bd0b during the audit; spot-checkable via the G-numbers

🤖 PR by Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.

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>
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Documentation
    • Added an EU-focused GDPR compliance audit covering current compliance strengths, identified gaps, unverified deployment details, and data-processing considerations.
    • Documented findings related to data deletion, transparency, consent, international transfers, AI profiling, data-subject rights, and security accountability.
    • Included a personal-data inventory and prioritized remediation roadmap.

Walkthrough

Changes

GDPR audit

Layer / File(s) Summary
Audit scope and existing evidence
docs/audits/gdpr-audit-2026-07.md
Adds audit metadata, applicability context, an executive summary, and an evidence table covering current compliance-relevant areas.
Findings and data inventory
docs/audits/gdpr-audit-2026-07.md
Documents GDPR gaps across retention, transfers, profiling, data-subject rights, security, accountability, and personal-data locations.
Remediation and verification
docs/audits/gdpr-audit-2026-07.md
Adds prioritized P0/P1/P2 remediation tasks and lists deployment details requiring verification.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: a GDPR audit doc with findings, ROPA, and a remediation roadmap.
Description check ✅ Passed The description accurately summarizes the added GDPR audit and its contents, and is clearly on-topic.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3895bd0 and 3c7affb.

📒 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 scope

Plan 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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

Comment on lines +102 to +109
**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`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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

Comment on lines +125 to +129
**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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant