12 — GDPR & Compliance Operations (DSGVO)

Purpose

Operational playbook for handling personal data (Mandanten-Stammdaten, Fragebogen-Antworten, Original-Dokumente, AI-derived artifacts) across the Knoll Analyzer stack and the GRAG platform: what lives where, how long it lives, how to export it (Auskunft, Art. 15 DSGVO), how to delete it end-to-end (Löschung, Art. 17 DSGVO), which processors need an AVV (Art. 28), how German PII reaches LLM providers today, and which technical/organisational measures (TOMs) the platform provides vs. what Knoll must add. This is plan task 7.5 groundwork; the go/no-go gate before any real Mandanten data is plan 0.11.

Not legal advice. This document is an engineering/operations checklist. Retention periods, AVV wording, and the lawful-basis analysis must be confirmed by the Kanzlei's counsel/DSB. Where a statutory period is mentioned it is context, not a determination.

Status / verified against: 2026-07-06, repo document-processing-pipelines @ db63a95, live instance https://app.grag.ai. Cross-references: 01-architecture.md, 03-id-conventions.md, 04-provisioning-runbook.md, 05-verification-runbook.md, 10-document-pipeline.md, 13-platform-gaps-issues.md.

Roles in DSGVO terms (to be confirmed by counsel): the Kanzlei is Verantwortlicher for Mandanten data; Knoll Analyzer (the product/operator) acts as Auftragsverarbeiter; the GRAG host operator, LLM providers (via BYOK), and the e-mail provider are Unterauftragsverarbeiter and need to appear in the AVV chain (§4 below).


1. Data inventory — what lives where

Env placeholders used throughout: $GRAG_URL=https://app.grag.ai, $GRAG_API_KEY (Knoll runtime key), $GRAG_TENANT (knoll in prod, knoll-dev in dev).

#Data categoryPersonal data?Where storedRetention (as deployed)Deletion path
1Client master data (company, contacts, contact details)YesKnoll Postgres (clients, contacts, plan 2.1)Contract lifetime + statutory commercial/tax retention (typically §257 HGB / §147 AO — confirm with counsel)SQL delete/pseudonymize (§3 step 8) + KG tombstone (§3 step 5)
2Questionnaire answers (16 chapters, free text — high PII density)YesKnoll Postgres (questionnaire_answers) + rendered markdown doc in the analysis-KB (→ rows 4, 5) + deterministic KG facts (row 6)Until deletion / analysis offboardingSQL delete + workspaces document DELETE with purge fan-out + KG reconcile (§3 steps 2, 5)
3Original-Dokumente (Jahresabschluss, NDA, Verträge…)YesKnoll file store (master, plan 2.4); GRAG keeps NO binaries — only derived text (rows 4, 5, 9)Until Löschung; statutory retention may apply to some doc typesFile-store object delete + per-document DELETE in workspaces (§3 steps 2, 7)
4Derived chunks / vectorsYes (chunk text = document text)voyager collection named by kb_id behind /ai-gateway/api/v1/retrieval/* (⚠ un-prefixed: TENANT_KEYS_ENABLED=false in .env.production.example:166) + document_points registry in GRAG postgres-platform (workspaces DB)Until document/KB purgePurge worker deletes voyager points from the document_points registry — only when WORKSPACES_DOCUMENT_PURGE_ENABLED=true (⚠ off as deployed; plan 1.4)
5KG document substrate (segments = full text, spans)YesGRAG postgres-platform, kg logical DB (kg_documents, kg_segments, kg_spans), RLS per tenantUntil purge / kg deletePurge worker soft-deletes via DELETE /kg-service/api/v1/documents/{id}; hard delete available with ?cascade=true
6KG entities & edges (Mandant, Empfehlung, Scores…)Yes (Mandant entities)kg_entities (relational, pgvector embedding) + Apache AGE graph kg_knoll; edges live ONLY in AGE (no relational copy)Indefinite — kg-service has NO entity delete route, edges have no delete route eitherTombstone: upsert properties.status="inaktiv" + filter in all consumers (plan 3.5); edges persist — documented platform limitation, see 13-platform-gaps-issues.md
7Conversations (chat turns; sources JSONB embeds full chunk text)YesGRAG postgres-platform, workspaces DB (conversations, conversation_messages)Until delete; DELETE is soft-only (deleted_at; cascade_delete_supported=false)DELETE /workspaces/api/v1/conversations/{id} (soft) + operator-level SQL hard delete for Art.-17 finality — platform gap, see §3 step 3
8LLM prompts/responses at providersYes (unredacted German text today — see §5)Provider side (OpenAI/Anthropic/… via BYOK keys in the ai-gateway keyvault)Per provider policy — TODO-VERIFY: retention terms of the chosen provider/endpoint (e.g. zero-data-retention option) before pilotNot deletable by us; mitigate upstream (§5)
9Transient job payloads (docfold markdown results, orchestrator job archive, DLQ)Yes (document text)GRAG Redis: docfold results (TODO-VERIFY exact TTL — "fetch promptly"), job archive 30 d (file_base64 >100 KB stripped), DLQ entries 30 d (contain original_payload + partial_results), chunking/enrichment job keys 24 hSelf-expiring (24 h–30 d)No per-item delete; rely on TTL. Note in the Löschkonzept: up to 30 d residual
10Kosten / AuditPseudonymous (tenant/ids, no content)GRAG ledger Postgres (cost_ledger_entries — retention "keep all rows forever"), capped Redis audit streams (~1000 entries: document_purge_audit, keyvault audit, verdict streams), lineage artifact rows (CAS ids, no payload), Knoll audit_logLedger: indefinite; streams: rolling cap; Knoll audit: per policyLedger purge is an operator cron (not built); Knoll audit_log retained deliberately as Löschnachweis
11PII vault (anonymization placeholders ↔ originals)Yes, if usedGRAG dpp-postgres-vault (separate cluster, AES-256-GCM, per-tenant HKDF DEKs)Currently empty for LLM traffic — firewall OFF in prod (§5)DELETE /anonymization/api/v1/vault/{id} (GDPR hard-delete, audit-logged)
12Agent-control event store (hashes-only transcripts)Hash+length only by defaultGRAG postgres-platform (agent_control_events)90 d (AGENT_CONTROL_EVENTS_RETENTION_DAYS); profile not started by the deploy workflow todayTTL-based; content capture stays off (double gate not set)
13Transactional e-mail (notifications, Dienstleister-Anfragen, plan 2.7)Yes (names, addresses)E-mail provider (Resend/SES — decision pending)Per provider policy — TODO-VERIFY once provider chosenProvider console / API; cover in AVV (§4)

Key takeaway: Knoll Postgres + Knoll file store are the masters; everything in GRAG is a derived projection except conversations (which are primary records of chat content) and the cost ledger (pseudonymous). The Löschung procedure must therefore fan out from grag_refs (plan 2.1), which maps every Knoll object to its GRAG ids.


2. Auskunft (Art. 15) — export procedure

Operationalisiert (2026-08-21): apps/web/scripts/gdpr-auskunft.ts (core: lib/gdpr/auskunft.ts) implements steps 1–4 + 7 below as one audited bundle — pnpm exec tsx scripts/gdpr-auskunft.ts "<Mandant>" writes db/*.json, the file-store originals, GRAG document inventories + conversation transcripts and a manifest, then logs data_export. Verified live on knoll-dev (bundle incl. GRAG inventories). The manual curl steps below remain the reference for anything the script does not cover.

Trigger: Mandant (via the Kanzlei) requests a copy of their data. Target: one export bundle per Mandant. All GRAG reads are ordinary tenant-scoped calls with the Knoll runtime key. Reference plan 7.5.

Step 1 — Knoll DB export. Export all rows keyed to the client_id: clients, contacts, analyses (+ milestones, contracts, analysis_team), questionnaire_answers, checklist_items, expert_reports, projects, calculation_line_items, leads, activities, ai_runs, files, grag_refs, relevant audit_log entries. A parameterised export script/server action belongs in the Knoll backend (Partner/Admin-only, itself audit-logged).

Step 2 — File-store originals. For each files row of the client: fetch the object (originals + generated expert-report/offer PDFs) into the bundle. GRAG holds no binaries, so this is the only source of the actual files (plan 2.4).

Step 3 — GRAG document inventory (per analysis-KB, ids from grag_refs):

curl -s "$GRAG_URL/workspaces/api/v1/kbs/$KB_ID/documents" \
  -H "Authorization: Bearer $GRAG_API_KEY" \
  -H "X-Tenant-ID: $GRAG_TENANT"

Include the returned metadata (name, mime, size, status, timestamps) as the processing record for that Analyse.

Step 4 — Conversations export (KB-chat 5.2, KIU chat 5.1b). List, then page messages ascending:

# list conversations of the Analyse-KB
curl -s "$GRAG_URL/workspaces/api/v1/conversations?kb_id=$KB_ID" \
  -H "Authorization: Bearer $GRAG_API_KEY" \
  -H "X-Tenant-ID: $GRAG_TENANT"

# full transcript per conversation (limit 1..1000, order=asc)
curl -s "$GRAG_URL/workspaces/api/v1/conversations/$CONV_ID/messages?order=asc&limit=1000" \
  -H "Authorization: Bearer $GRAG_API_KEY" \
  -H "X-Tenant-ID: $GRAG_TENANT"

Note: assistant messages carry sources/trace/groundedness JSONB — the sources include chunk text from the Mandant's documents; include them (they are the Mandant's data anyway).

Step 5 — KG view of the client (kg-service additionally requires X-Workspace-ID):

curl -s "$GRAG_URL/kg-service/api/v1/entities/client:$CLIENT_SLUG" \
  -H "Authorization: Bearer $GRAG_API_KEY" \
  -H "X-Tenant-ID: $GRAG_TENANT" \
  -H "X-Workspace-ID: $WORKSPACE_ID"

curl -s "$GRAG_URL/kg-service/api/v1/entities/client:$CLIENT_SLUG/neighborhood?depth=2" \
  -H "Authorization: Bearer $GRAG_API_KEY" \
  -H "X-Tenant-ID: $GRAG_TENANT" \
  -H "X-Workspace-ID: $WORKSPACE_ID"

(These are projections of Knoll DB facts — step 1 already covers the content; include for completeness of the processing record.)

Step 6 — (optional) per-analysis cost record, if the firm passes costs through: GET $GRAG_URL/ledger/api/v1/ledger/totals?pipeline_id=$ANALYSIS_RUN_ID (same auth headers; per-analysis attribution requires the X-Pipeline-Id stamping from plan 7.3).

Step 7 — Log it. Write a data_export event to audit_log (who, when, scope, delivery channel). Deliver via a secure channel (not plain e-mail attachment).


3. Löschung (Art. 17) — ordered deletion checklist

Operationalisiert (2026-08-21): apps/web/scripts/gdpr-loeschung.ts (core: lib/gdpr/loeschung.ts) plans and executes the checklist below — DRY-RUN by default, destructive only with --execute; every run writes deletion_started/deletion_completed audit entries incl. residuals. Live finding on knoll-dev (2026-08-21, filed as platform issue #406): with the purge flags ineffective the deletes are metadata-level only — the KB vanished, but its document row stayed indexed and voyager kept serving its chunks. The verification probes below (step 11) caught this — treat them as mandatory, and treat the flags precondition as hard.

Scope variants: single document, single Analyse, full Mandant offboarding (shown here — the superset; plan 3.2). Work strictly from the grag_refs inventory.

⚠ Preconditions (current-state caveats — blocking, plan 0.11 / 1.4 / 1.13):

  • WORKSPACES_DOCUMENT_PURGE_ENABLED and LINEAGE_CLEANUP_ENABLED are unset → compose defaults false as deployed: document/KB DELETE is soft-delete only — voyager points, kg segments, and lineage rows remain (deleted docs are merely hidden by the chat live-document filter). Flipping both is plan 1.4 and blocking for real data. Verify live state first (plan 0.9).
  • Backups: deleted data persists in backups until backup retention expires (PITR retention is 14 days once enabled, §6). State this in the Löschkonzept.
  • Statutory retention (HGB/AO) may forbid deleting some Knoll DB records (Verträge, Rechnungsdaten): pseudonymize instead of delete where counsel says retention applies.

Checklist (execute in this order, record each step in audit_log):

  1. Freeze Knoll-side. Mark the client Blocked (no new runs, no chat), snapshot the grag_refs rows for this client (they are your deletion worklist and later the deletion log).

  2. Per-document DELETE with purge fan-out. For every document in every analysis-KB of the client (list via GET /workspaces/api/v1/kbs/{kb}/documents):

    curl -s -X DELETE "$GRAG_URL/workspaces/api/v1/documents/$DOC_ID" \
      -H "Authorization: Bearer $GRAG_API_KEY" \
      -H "X-Tenant-ID: $GRAG_TENANT"
    

    With the purge flag on, the workspaces-worker then (per docs/runbooks/document-purge.md): deletes the voyager points from the document_points registry → soft-deletes kg segments (DELETE /kg-service/api/v1/documents/{id}) → cascade-deletes the lineage subtree (DELETE /lineage/api/v1/artifact/{docfold_id}?cascade=true) → drops registry rows. Fail-closed: 5xx leaves the job in the consumer group and it replays; watch workspaces_document_purge_total{tenant,outcome} and the t:{tenant}:document_purge_audit stream. Documents ingested before the points registry existed need the backfill/re-ingest path from the purge runbook before purge can find their points.

  3. KB delete per analysis. DELETE $GRAG_URL/workspaces/api/v1/kbs/$KB_ID (same headers) — triggers the voyager/kg/lineage purge fan-out for the collection when the flag is on.

  4. Conversations. DELETE $GRAG_URL/workspaces/api/v1/conversations/$CONV_ID for each — soft-delete only (cascade_delete_supported=false; message rows incl. embedded chunk text remain in Postgres). For Art.-17 finality an operator-level SQL hard delete on conversations/conversation_messages is currently required — tracked as a platform gap in 13-platform-gaps-issues.md.

  5. Lineage belt-and-braces. For any grag_refs artifact ids not covered by step 2's fan-out (e.g. directly ingested questionnaire markdown, expert-report PDFs):

    curl -s -X DELETE "$GRAG_URL/lineage/api/v1/artifact/$ARTIFACT_ID?cascade=true" \
      -H "Authorization: Bearer $GRAG_API_KEY" \
      -H "X-Tenant-ID: $GRAG_TENANT"
    

    This marks the forward subtree; the destructive fan-out additionally requires LINEAGE_CLEANUP_ENABLED=true (1.4). Hard delete of lineage rows is admin-only with confirm token (operator action).

  6. KG tombstoning — there is NO delete route. kg-service exposes no entity delete and no edge delete. Tombstone instead (plan 3.5/3.6):

    curl -s -X POST "$GRAG_URL/kg-service/api/v1/entities/upsert" \
      -H "Authorization: Bearer $GRAG_API_KEY" \
      -H "X-Tenant-ID: $GRAG_TENANT" \
      -H "X-Workspace-ID: $WORKSPACE_ID" \
      -H "Content-Type: application/json" \
      -d '{"entities":[{"entity_id":"client:'$CLIENT_SLUG'","entity_type":"Client","label":"[deleted]","aliases":[],"description":"","properties":{"status":"inactive","deleted_at":"2026-07-06"}}]}'
    

    Overwrite label/description/aliases with neutral values (the upsert replaces them) and tombstone every client-specific entity (recommendations, projects of this client). Residual: AGE edges (SCORES, RECOMMENDS, …) persist with their properties (scores, analysis ids — pseudonymous but present); all consumers must filter status="inactive". Documented platform limitation → 13-platform-gaps-issues.md; a real fix needs the plan 1.11 write route to grow a delete/retract counterpart.

  7. Workspace delete. DELETE $GRAG_URL/workspaces/api/v1/workspaces/$WORKSPACE_ID?cascade=true — executable with the Knoll runtime key (NOT admin-gated; in workspaces only tenant create/patch/delete are). If cascade deletion should be operator-only, enforce that as Knoll-side policy. Tenant soft-delete has NO composite purge — do not rely on it.

  8. Knoll file store. Delete all objects for the client's files rows (originals + generated PDFs).

  9. Knoll Postgres. Delete/pseudonymize the client's rows (respecting statutory retention, see preconditions). Keep audit_log and a minimal grag_refs tombstone as the deletion proof.

  10. PII vault (only if the firewall was ever enabled for tenant knoll): DELETE $GRAG_URL/anonymization/api/v1/vault/$VAULT_ID per recorded vault id (audit-logged hard delete).

  11. Verification probe (also in 05-verification-runbook.md; make it part of the 7.2 canary where feasible):

    # (a) semantic probe: a distinctive phrase from a deleted document must return 0 hits
    curl -s -X POST "$GRAG_URL/ai-gateway/api/v1/retrieval/collections/$KB_ID/search" \
      -H "Authorization: Bearer $GRAG_API_KEY" \
      -H "X-Tenant-ID: $GRAG_TENANT" \
      -H "Content-Type: application/json" \
      -d '{"query_text":"<distinctive phrase from the deleted doc>","top_k":5}'
    # expect: no hit whose payload doc_id is the deleted document (or 404 if the collection was deleted)
    
    # (b) point count: collection info after KB purge
    curl -s "$GRAG_URL/ai-gateway/api/v1/retrieval/collections/$KB_ID/info" \
      -H "Authorization: Bearer $GRAG_API_KEY" -H "X-Tenant-ID: $GRAG_TENANT"
    
    # (c) kg segment gone (expect 404)
    curl -s "$GRAG_URL/kg-service/api/v1/documents/$DOC_ID/segments/0/text" \
      -H "Authorization: Bearer $GRAG_API_KEY" -H "X-Tenant-ID: $GRAG_TENANT" \
      -H "X-Workspace-ID: $WORKSPACE_ID"
    
    # (d) inventory shows deleted
    curl -s "$GRAG_URL/workspaces/api/v1/documents/$DOC_ID?include_deleted=true" \
      -H "Authorization: Bearer $GRAG_API_KEY" -H "X-Tenant-ID: $GRAG_TENANT"
    

    Plus operator-side: document_purge_audit stream entries show ok=1 with points_deleted/kg_deleted/lineage_cascaded counts.

  12. Deletion log. Write a summarising deletion_completed event to audit_log (scope, ids, probe results, residuals: Redis TTL windows ≤30 d, backup retention, AGE edges tombstoned-not-deleted, LLM provider side per §5).


4. Processors & AVV (Art. 28)

ProcessorRoleWhat the AVV/DPA must cover
GRAG host operator (single Plesk box plesk.easy-dashboard.de, 64 GB, ~90 containers; hosts postgres-platform, vault, Redis, voyager)Unterauftragsverarbeiter (infrastructure + platform ops)Physical location & jurisdiction — TODO-VERIFY: datacenter location/hosting contract of the Plesk box is not documented in-repo (deployment report, open question 2); admin access model (GitHub Actions self-hosted runner deploys on push to main; operator secrets in GitHub Secrets); backup handling & restore (§6); incident notification duty; subprocessors of the host
LLM providers via BYOK (OpenAI/Anthropic/…, keys in the ai-gateway keyvault)Unterauftragsverarbeiter (content processing)Which endpoints (EU data-residency options where offered), retention/zero-data-retention terms, no-training clauses, Art. 44 transfer mechanism if non-EU — TODO-VERIFY per chosen provider before pilot (plan 1.10 decision)
E-mail provider (Resend/SES/…, plan 2.7)Unterauftragsverarbeiter (notifications)Region/EU processing, retention of message content & logs, subprocessors
Knoll file store / object storage host (plan 2.4, D2 hosting decision)Part of Knoll's own processing or a further subprocessor if externally hostedEncryption at rest, region, backup handling

Also required: Verzeichnis von Verarbeitungstätigkeiten (Art. 30) covering the flows in §1, and a Löschkonzept referencing §3 including the residuals (Redis TTLs, backups, AGE edges, provider side).


5. The German-PII-to-LLM problem (plan 1.10 — decide before pilot)

Current state (verified): every LLM call (chat, Gutachten pipeline, extraction) sends unredacted German text to the configured provider. The platform's anonymization firewall is (a) OFF in prod (GATEWAY_FIREWALL_ENABLED=false, .env.production.example:60; guardrail also off, :92) and (b) English-only at the API boundary (language: Literal["en"] in anonymization/src/anonymization/api/schemas/requests.py:24; ADR 0003 defers German to v2), even though the underlying GLiNER2 PII model is multilingual (42 labels × 7 languages incl. German).

Options from plan 1.10:

OptionWhat it takesAssessment
(a) German anonymization v2 + firewall forced on for tenant knollPlatform work: lift the Literal["en"] boundary (model already multilingual), soak the firewall, per-tenant enforcement (TODO-VERIFY: the exact per-tenant firewall-forcing mechanism/setting — candidates are the ADR 0030 tenant-settings firewall keys)Cleanest end state; placeholder redaction may degrade German Gutachten fluency — needs quality eval on the 7.1 corpus
(b) Documented accepted risk + EU provider endpoints via BYOK + contractual guarantees (DPA, zero/short retention, no training)Keyvault entry per provider (POST /ai-gateway/api/v1/keys), provider/endpoint selection, AVV chain (§4), Verantwortlicher sign-offFast; the personal data still reaches the provider — the protection is contractual + jurisdictional, not technical
(c) Knoll-side pseudonymization before every LLM callKnoll backend redacts (deterministic mapping in Knoll DB) before ai-gateway, restores after; the platform's X-Anon-Already-Redacted carry-vault pattern exists for this directionNo platform dependency, but duplicates a platform capability and risks worse recall than the tuned detector stack; touches every prompt-building path (5.1/5.2/5.3)

Recommendation: for the pilot (plan 8.3, which uses one anonymized Mandanten-Dossier anyway): option (b) — EU endpoints via BYOK + DPA + documented accepted risk, signed off by the Kanzlei as Verantwortlicher. Target end state before scaling beyond the pilot: option (a), gated on a German redaction-quality check against the 7.1 corpus. Keep (c) as fallback only if (a) stalls. Record the decision in plan 0.11's go/no-go list.


6. Backups (plan 1.13 — blocking for real data)

Current state (verified): the deploy workflow starts neither the pg_dump backup profile nor pitr/pitr-vault (COMPOSE_PROFILES: retrieval only, deploy.yml:108), despite the compose comment "Prod deployments MUST enable this profile or wire an equivalent external backup". The host-maintenance.yml schedule comment references live base-backup windows (02:00 platform / 04:00 vault), so an operator may have brought PITR up manually — ⚠ verify with docker compose ps on the box (plan 0.9/0.11). Until confirmed, assume: no automated backup of Mandanten-bearing data.

Required end state:

ItemTarget
GRAG postgres-platform (workspaces, kg, ledger, lineage…)pitr profile: WAL-G continuous WAL + nightly base backups, libsodium-encrypted (WALG_LIBSODIUM_KEY), 14-day retention (ADR 0015). Bucket location must be documented for §4 (TODO-VERIFY: MinIO-local vs external S3 and its region)
GRAG PII vault clusterpitr-vault profile: separate bucket + separate key (WALG_VAULT_LIBSODIUM_KEY); restore REQUIRES the KEK-decrypt smoke test (smoke_decrypt.py) — a restore across a KEK rotation is silently unreadable otherwise (ADR 0016)
Alternative/minimumbackup profile (pg_dump sidecar) or an equivalent external dump job
Knoll Postgres + Knoll file storeOwn backup plan on the Knoll host (D2 decision explicitly includes disk sizing + backup plan, plan 0.2) — same encryption/retention discipline
Restore testsOnce at enablement (plan 1.13 "verify restore once"), then quarterly: platform PITR restore to the restore profile volume + row-count sanity; vault restore + KEK smoke test; Knoll DB dump restore + app boot
Backups vs Art. 17Deleted data persists in backups until retention expiry (14 d PITR). Document in the Löschkonzept; do NOT restore deleted Mandanten data without re-running §3 afterwards

7. TOMs checklist (Art. 32) — platform-provided vs Knoll-owned

TOMProvided byStatus / action
Tenant isolation: Postgres RLS (ENABLE+FORCE, SET LOCAL app.tenant_id on every pool checkout) on all tenant-bearing tables; per-tenant AGE graph; tenant in CAS pre-imageGRAG (ADR 0013/0001/0025)Active. Caveat: Redis + voyager prefixes gated on TENANT_KEYS_ENABLEDfalse as deployed; cutover required before a second real tenant (plan 1.5)
Cross-tenant probes answer 404, never 403 (no existence leak)GRAGActive
Per-tenant cryptography: AES-256-GCM with HKDF-derived DEKs, tenant as AAD (BYOK keyvault + PII vault; separate vault Postgres cluster/trust boundary)GRAG (ADR 0004/0029)Active where those stores are used; BYOK secrets are write-only (last4 hint only)
Transport encryptionPlesk nginx terminates public TLS for app.grag.ai; Traefik internal on localhostActive. Knoll host needs equivalent TLS
BFF authentication/next/api/* currently unauthenticated (trusts any inbound X-Tenant-ID, injects platform key server-side)— (platform gap)BLOCKER, plan 1.8; regression probe expecting 401 in plan 0.10a / 7.2
Key custody: admin/runtime split via singular env names WORKSPACES_ADMIN_API_KEY / MCP_GATEWAY_ADMIN_API_KEY (setting the plural names in .env does nothing); dedicated Knoll key in PIPELINE_API_KEY list + named GATEWAY_CALLERS entry; never the service keyOperator + Knoll (plan 1.1/1.2, D5)To do; verify post-deploy: tenant create with runtime key must 403
GRAG key never reaches the browser; server-side only (packages/grag-client)Knoll (plan 2.8/7.7)To build; grag-client hard-fails on missing X-Tenant-ID (platform allow_default_tenant=true everywhere — a dropped header silently lands in tenant default)
Budget/rate governance: BYOK monthly_budget_usd + rate_limit_rpm with hard 402/429; Traefik 100 r/s per IPGRAG (ADR 0029)Configure in plan 1.6
Rate-limited vault reveal: /anonymization/api/v1/vault/{id}/reveal + /deanonymize limited to 10/min (burst 5) per source IP at Traefik; every reveal/delete audit-loggedGRAGActive (relevant once §5 option (a) lands)
Audit streams: tenant_settings_audit (Postgres, append-only), t:{tenant}:document_purge_audit, keyvault audit (key id + action + last4 only), safety/groundedness verdict streams — all content-free by designGRAGActive; counts/verdicts only, never user content
Access control on people: users, roles Partner/Analyst/Backoffice (+ Admin), Partner-only gates (Gutachten-Freigabe, Honorare) enforced server-sideKnoll (plan 2.3/7.7)To build — GRAG authorizes keys, not people; it has NO user model
Monitoring/alerting: purge outcome metric, kg_service_age_mirror_failures_total, ledger spend alert at 80 %, uptime probesGRAG metrics + Knoll alerting (plan 7.3/7.6)To wire
Backups + restore testsOperator + Knoll§6 — currently NOT ensured

8. Knoll-side audit trail (audit_log, plan 2.1)

Write append-only events (actor user id, role, timestamp, ip/user-agent, object type + id, action, outcome, optional diff-summary — never document content). Events worth writing:

DomainEvents
AuthN/AuthZlogin success/failure, logout, password/SSO changes, role change, permission-denied on Partner-only gates
Client/analysis lifecycleclient created/changed/blocked/deleted; analysis created/phase-changed/completed; GRAG provisioning + deprovisioning calls with returned ids (mirrors grag_refs)
Documentsupload (files id + grag document_id), download (who pulled which original — Art.-32 need-to-know evidence), delete, supersede
Questionnairesubmitted, re-submitted (versions), rendered-to-KB (document_id)
Expert reportpipeline step start/finish (ai_runs id, model, tokens, cost, groundedness band), Partner approval (who, when, version) — the QMS human-in-the-loop record
AI/GRAG callsper ai_runs row: endpoint class, X-Pipeline-Id, outcome, error taxonomy class (401/403/402/429/503) — no prompt content
GDPR operationsdata_export (scope, delivery), deletion_started / each §3 step / deletion_completed (probe results, residuals), consent/AVV references
Settings/Emailknoll_ai_settings changes, fee changes (Partner-only), notification sends (template id, recipient)

Retention of audit_log itself: keep beyond client deletion as deletion proof (pseudonymize actor references only if required) — confirm period with counsel.


9. Open items feeding plan 0.11 (GDPR go/no-go)

  1. plan 1.8 BFF auth fixed + 401 probe green (0.10a).
  2. plan 1.4 purge flags (WORKSPACES_DOCUMENT_PURGE_ENABLED, LINEAGE_CLEANUP_ENABLED) verified on live; §3 step 11 probe passes on a fixture document.
  3. plan 1.13 backups running + first restore test done (§6), incl. TODO-VERIFY on the live PITR state.
  4. §4 AVV chain signed, incl. TODO-VERIFY host location.
  5. §5 German-PII decision recorded and implemented (1.10).
  6. Conversation hard-delete gap and KG edge-deletion gap accepted as documented residual risks or fixed platform-side (13-platform-gaps-issues.md).