AI · Travel RAG case study · Part 5 of 5
Regulatory compliance: mapping GDPR and the EU AI Act onto a RAG system
Most writing about AI and EU law comes from one of two places. Lawyers explain the law to engineers, which is strong on law and thin on systems. Engineers add a cookie banner and call it compliance, which is thin on both. I wanted the third version: one real stack, every place user data lands, and an honest list of what is clean and what is not.
This is a deep-dive from my travel RAG case study. My bachelor thesis was on EU AI regulation, so this project was a chance to test the theory against code. The short answer: the regulation turned out to be a set of architecture decisions. Where data rests, what you store, which vendor sees what, and what the user is told are all choices you make in the design, long before anyone asks for a policy page.
I am not a lawyer. This is an engineer's reading of the rules as of October 2026, applied to one system.
1.Planning
Before building I asked three questions. Each one turned into a design constraint.
Question 1: where does user data land?
GDPR was written for systems that store what you put in them. A language model mostly does not, but the system around it stores a lot: traces, logs, analytics, rate-limit keys and exported documents. So I treated every place user data lands as its own processing activity, with its own reason to exist and its own retention.
Question 2: what risk tier is this system?
The EU AI Act sorts systems by what they are used for, not by what model they run.

A travel assistant makes no decision about anyone's job, credit or legal status, so it is not high risk. It is a chatbot that generates text, which puts it in the limited-risk tier. The obligation there is transparency under Article 50, which has applied since 2 August 2026: people must know they are talking to an AI. The Digital Omnibus agreed this year delayed the high-risk deadlines to December 2027 and August 2028, but it did not move Article 50 (Jones Walker).
GDPR's Article 22 draws a related line: people can object to decisions made solely by automated processing that have legal or similarly significant effects. What triggers it is the effect, not the use of AI. A trip suggestion has no such effect; the same RAG pipeline used for loan or hiring decisions would.
The Act also has a separate set of rules for providers of general-purpose models like Claude or GPT-OSS. Those duties sit with Anthropic and OpenAI, not with me. Building on top of a model does not make you its provider, and confusing the two is a quick way to lose credibility.
Question 3: where does each vendor process data?
For every external service I asked where data travels, where it rests and what the contract says about it. That question shaped the stack: the database, the LLM traces and the analytics all run in EU regions. The answer for the model layer is less tidy, and section 2 shows it honestly.
2.Implementation
Every place user data lands
The app has no accounts: everyone is anonymous, and Auth0 is built but deliberately not wired in. That keeps the personal data small, but not zero. A user's question is free text and can contain anything, including a name. Here is where it goes.

| Surface | What it holds | Where | Why it exists |
|---|---|---|---|
| Model call | The question plus retrieved chunks | US (Anthropic; Groq if chosen) | Generating the answer |
| Embedding call | The question text | US (Voyage) | Searching the corpus |
| LLM traces | Prompts, chunks and answers | EU (Langfuse) | Debugging and evals |
| Error events | Stack traces, headers scrubbed | Germany (Sentry) | Fixing bugs |
| Product analytics | 10 named events, no autocapture | EU (PostHog) | Understanding usage |
| Rate-limit keys | IP address, for one minute | EU (Upstash, Frankfurt) | Abuse protection |
| Vector store | Only Wikivoyage content | EU (Neon) | Retrieval; no user content is embedded |
| Notion export | The generated itinerary | US (Notion) | Only when the user clicks export; the page is published on a public TravelRAG Trips site |
| Map and weather lookups | Place names from the answer, and their coordinates | US (Mapbox); Open-Meteo | Map pins and trip weather; no question text is sent |
Vendors: the realistic picture
The data layer is EU-resident: the backend, the database, the traces, the error reports and the analytics all run in Frankfurt or EU regions. The model layer is not. Anthropic, Groq and Voyage process requests in the US under Standard Contractual Clauses. Anthropic's commercial terms exclude training on API inputs.
That split is the honest state of LLM products in Europe in 2026. A fully EU stack means Mistral or a self-hosted open-weight model, both with quality trade-offs. Saying "EU data layer, US model layer under SCCs" is more credible than claiming "fully EU".
Privacy by design, in code
- No user content in the vector store. The index holds only public Wikivoyage text, so there is nothing personal to erase from it.
- Scrubbing at the edges. The backend's Sentry hook strips
Authorizationandx-api-keyheaders. A shared frontend helper removes OAuth tokens from URLs before Sentry or PostHog sees them. Database errors are scrubbed of connection strings before they are raised. - One seam for traces. Every LLM call goes through one tracing module, so input filtering, when it is needed, lands in one place.
- Analytics by allow-list. PostHog's autocapture is off. Only ten named events are sent, such as
chat_message_sentandnotion_export_completed.
Transparency under the AI Act
Article 50 asks that people know they are dealing with an AI. The product does that through its design, not a disclaimer:
- The header dropdown names the model: "Claude (Anthropic)" or "Groq (GPT OSS 120B)".
- Every answer carries a provider badge.
- There is no persona. The app never pretends to be a human travel agent.

Transparency in the product itself: the header names the AI model behind every answer.
3.Challenges
The clean parts were design choices. The hard parts are questions an LLM stack raises that the rules do not settle neatly.
| Question | Why it is hard | Where I landed |
|---|---|---|
| Can you honour a deletion request? | Without accounts, I cannot tell which traces belong to whom | Short retention instead of per-person deletion; accounts would add a real erasure flow |
| What if users type personal data? | Free text goes straight into prompts, traces and the model call | Accepted for an anonymous demo; filtering at the tracing seam before accounts launch (TD-059) |
| Do upstream providers keep anything? | Model and embedding APIs have their own retention terms | Use commercial terms with no training on inputs; document each vendor's window |
| Are analytics allowed without consent? | Browser storage for analytics usually needs consent under the ePrivacy rules | Gap: no consent banner yet |
| Can I serve the guidebooks I bought? | Licensed for reading, not for redistribution | Public product rebuilt on Wikivoyage; the licensed corpus stays in a separate database that is never deployed |
| Must generated text carry a machine-readable mark? | Article 50(2) targets providers of systems that generate content; how it applies to an app built on someone else's model is not settled | Open question; systems on the market before 2 August 2026 have until 2 December 2026 |
Anonymity cuts both ways
No accounts means almost no personal data, which is good. It also means I cannot answer "show me or delete everything you hold about me": nothing links one person's requests together. The practical answer is short retention everywhere: rate-limit keys expire after a minute, and traces should expire on a fixed schedule. Once accounts exist, erasure becomes a concrete flow: delete the user's rows, purge their trace IDs, and confirm nothing user-derived was embedded.
Free text is personal data waiting to happen
A travel question rarely contains personal data. But "plan a trip for my mother Anna, who uses a wheelchair" contains a name and health information, and the whole prompt lands in the model call and in Langfuse. For an anonymous demo I accepted that. Before any real user data flows, input filtering belongs in the one tracing module every call passes through.
Copyright is a data-architecture decision
The licensed guidebooks were the best corpus I had, and I could not serve them. A filter in code would not do: a bug or a bad config could leak licensed text. So the public deployment points at a separate Postgres database that does not contain the licensed content at all. Wikivoyage is CC BY-SA, which requires attribution; every answer cites the articles it used.
Analytics need consent, not just EU hosting
Hosting PostHog in the EU solves residency. It does not solve consent. Its browser SDK stores an identifier on the device, which under the ePrivacy rules generally needs consent unless it is strictly necessary. Product analytics are not. The fix is a consent banner, or switching to cookieless, memory-only analytics.
4.Final results
The system is clean where design choices could make it clean, and has a short, named list of gaps where production work remains. I'd rather publish that list than claim "fully GDPR compliant".
| Area | Status | Detail |
|---|---|---|
| AI Act risk tier | Done | Limited risk; transparency met by product naming, the model dropdown and provider badges |
| EU data layer | Done | Backend, database, traces, errors and analytics in Frankfurt or EU regions |
| Model layer | Documented | US processing under Standard Contractual Clauses; no training on API inputs |
| Data minimisation | Done | No accounts; no user content embedded; analytics by allow-list; rate-limit keys expire in a minute |
| Secret and token scrubbing | Done | Headers, URL tokens and database credentials removed before telemetry |
| Copyright | Done | Licensed corpus physically separated and never deployed; public corpus attributed |
| Consent for analytics | Gap | No consent banner yet |
| Privacy policy | Gap | No page yet listing which provider sees which data |
| Free-text personal data in traces | Gap | Filtering deferred until accounts launch (TD-059) |
| Trace retention | Gap | No fixed expiry set yet |
| Vendor agreements | Gap | Data processing terms not yet audited vendor by vendor |
| Data subject requests | Not applicable yet | No accounts, so no access or erasure flow; designed for when Auth0 is wired in |
None of the gaps needs a redesign. Each is a page, a banner, a filter at an existing seam or a setting. That is the payoff of making the architectural choices early.
5.Learnings

- Regulation is architecture. Almost every obligation was answered by a choice about where data rests, what is stored or what the UI shows. Very little needed legal text.
- List every place data lands. The model is not where most user data lives. Traces, logs, analytics and exports are. Each is a processing activity with its own purpose and retention.
- Classify by use, not by technology. The same RAG pipeline is limited risk for travel tips and high risk for hiring. Know which side of the line your use case sits on before you build.
- Know where you sit in the value chain. Building on Claude makes you a deployer, not a general-purpose model provider. The obligations follow the role.
- Be honest about the model layer. An EU data layer with US models under SCCs is the real state of 2026. Saying so is more credible than claiming "fully EU".
- Physical beats logical for hard boundaries. For a legal line, such as licensed content that must never be served, separate the data. Do not rely on a filter.
- Publish the gap list. A short list of what is not done yet, each item tied to a fix, shows more understanding than a claim of full compliance.
The thesis taught me the framework. This project taught me that the framework only becomes real when you have to decide where a single trace is stored.