Huyen TranProduct Manager · AI Engineer

// 006 — Essay

Back to main

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.

EU AI Act risk tiers, simplified · my reading
Figure 1. EU AI Act risk tiers, simplified · my reading

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.

where user data travels · deployed stack, October 2026
Figure 2. where user data travels · deployed stack, October 2026
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 Authorization and x-api-key headers. 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_sent and notion_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.
Figure 3. Transparency in the product itself: the header names the AI model behind every answer.

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

rules mapped to design decisions · sections 2 and 3
Figure 4. rules mapped to design decisions · sections 2 and 3
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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".
  6. 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.
  7. 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.