All articles

AI Agents13 min read

EU AI Act and GDPR Compliance for RAG Systems on Google Cloud

The exact GCP architecture used to build AI systems that satisfy EU AI Act risk classification, GDPR data residency requirements, and NIS2 audit obligations — without slowing down development

  • EU AI Act
  • GDPR
  • RAG
  • Vertex AI
  • Google Cloud
  • Compliance
  • Data Residency
  • BigQuery

Formare AI designs GDPR-compliant and EU AI Act-ready AI systems on Google Cloud Platform. By combining Vertex AI data residency controls, BigQuery audit logging, and structured explainability layers in RAG architectures, these systems meet risk classification requirements from day one — reducing legal exposure while maintaining full development velocity.

The EU AI Act entered phased enforcement in 2024–2026. General-purpose AI systems and high-risk AI applications (those affecting employment, credit, healthcare, or critical infrastructure) face the strictest obligations. GDPR fines remain separate: up to 4% of global annual turnover per violation. Many RAG systems used in enterprise settings qualify as "high-risk" under both frameworks simultaneously.

What "Compliant" Actually Requires for RAG Systems

Most teams treat compliance as a legal checkbox — something that gets added after the system is built. That approach fails because the core architectural decisions (where data is stored, how model outputs are logged, whether retrieved chunks can be traced back to their source) are nearly impossible to retrofit. Compliance must be designed in from the start. There are five distinct requirements a RAG system must satisfy under the combined EU AI Act + GDPR framework:

  • Data residency: all personal data and model inputs/outputs must remain in the EU (Article 46 GDPR)
  • Right to erasure: retrieved document chunks that include personal data must be deletable and that deletion must propagate to the vector index
  • Explainability: the system must be able to explain which documents supported a given response (EU AI Act Annex IV)
  • Audit trail: every model call, retrieved chunk, and user interaction must be logged immutably (NIS2 + EU AI Act Article 12)
  • Consent and purpose limitation: personal data used in RAG retrieval must have valid legal basis and cannot be used for unrelated purposes

Requirement 1: EU Data Residency on Google Cloud

Google Cloud's data residency guarantees are enforced at the resource level, not the account level. For GDPR compliance, every GCP resource that touches personal data must be explicitly constrained to EU regions. Use an Organization Policy to enforce this at the project level — this prevents accidental provisioning of resources outside the EU:

bash
1# Enforce EU-only resource creation via Organization Policy
2# Applied at the project level — prevents any resource outside EU regions
3gcloud resource-manager org-policies set-policy eu-residency-policy.yaml \
4  --project=your-project-id
5
6# eu-residency-policy.yaml content:
7# constraint: constraints/gcp.resourceLocations
8# listPolicy:
9#   allowedValues:
10#     - in:eu-locations   # Includes: europe-west1, europe-west3, europe-west4, europe-north1
11
12# Verify Vertex AI model endpoint region (must be EU)
13gcloud ai endpoints list --region=europe-west1
14
15# BigQuery dataset location — set at creation, cannot be changed
16bq mk --dataset --location=EU your-project:production_rag
17
18# Vertex AI Vector Search index — also EU-region constrained
19gcloud ai index-endpoints create \
20  --display-name="eu-rag-index" \
21  --region=europe-west1

Requirement 2: Right to Erasure in the Vector Index

This is the most technically challenging GDPR requirement for RAG systems. When a user invokes the right to erasure, you must delete their personal data from: (1) the source document store, (2) the BigQuery staging tables, and (3) the vector index. Vertex AI Vector Search supports individual datapoint removal — use this to implement a complete erasure pipeline:

python
1from google.cloud import aiplatform, bigquery
2import json
3
4def execute_right_to_erasure(user_id: str, index_id: str, bq_dataset: str):
5    """
6    Complete GDPR erasure pipeline for a given user_id.
7    Step 1: Find all vector index datapoint IDs belonging to this user.
8    Step 2: Remove datapoints from Vertex AI Vector Search index.
9    Step 3: Delete source rows from BigQuery staging tables.
10    Step 4: Log the erasure event (for compliance audit).
11    """
12    bq_client = bigquery.Client()
13
14    # Step 1: Find all chunk IDs for this user in BigQuery
15    query = f"""
16    SELECT chunk_id, source_document_id
17    FROM `{bq_dataset}.document_chunks`
18    WHERE owner_user_id = @user_id
19    AND gdpr_erased = FALSE
20    """
21    job_config = bigquery.QueryJobConfig(
22        query_parameters=[bigquery.ScalarQueryParameter("user_id", "STRING", user_id)]
23    )
24    rows = list(bq_client.query(query, job_config=job_config).result())
25    chunk_ids = [r["chunk_id"] for r in rows]
26
27    if not chunk_ids:
28        return {"status": "no_data_found", "user_id": user_id}
29
30    # Step 2: Remove from Vertex AI Vector Search
31    index = aiplatform.MatchingEngineIndex(index_name=index_id)
32    index.remove_datapoints(datapoint_ids=chunk_ids)
33
34    # Step 3: Mark as erased in BigQuery (soft-delete with timestamp)
35    # Physical delete violates audit trail requirements — use soft-delete
36    update_query = f"""
37    UPDATE `{bq_dataset}.document_chunks`
38    SET
39      gdpr_erased = TRUE,
40      gdpr_erased_at = CURRENT_TIMESTAMP(),
41      chunk_text = NULL,     -- Wipe the personal data
42      embedding = NULL        -- Wipe the embedding
43    WHERE owner_user_id = @user_id
44    """
45    bq_client.query(update_query, job_config=job_config).result()
46
47    # Step 4: Audit log the erasure
48    audit_row = [{
49        "event_type": "GDPR_ERASURE",
50        "user_id": user_id,
51        "chunks_erased": len(chunk_ids),
52        "timestamp": bigquery.enums.SqlTypeNames.TIMESTAMP,
53        "requested_by": "user",
54        "legal_basis": "GDPR Art. 17 Right to Erasure",
55    }]
56    bq_client.insert_rows_json(f"{bq_dataset}.gdpr_audit_log", audit_row)
57
58    return {"status": "erased", "chunks_erased": len(chunk_ids)}

Requirement 3: Source Explainability — Which Chunks Supported This Answer?

The EU AI Act (Annex IV) requires high-risk AI systems to provide explanations for outputs that affect individuals. For RAG systems, this means the system must be able to answer: "Which specific documents did you use to produce that response?" Implement this by storing the retrieved chunk IDs alongside every model response in the audit log:

python
1from vertexai.generative_models import GenerativeModel
2from google.cloud import bigquery
3import datetime, json, uuid
4
5def rag_query_with_explainability(
6    user_query: str,
7    user_id: str,
8    index_endpoint,
9    bq_audit_table: str,
10) -> dict:
11    """
12    Performs RAG retrieval and generation with full explainability logging.
13    Returns both the answer and the source citations.
14    """
15    # Step 1: Retrieve relevant chunks from Vector Search
16    response = index_endpoint.find_neighbors(
17        queries=[embed(user_query)],
18        num_neighbors=5,
19    )
20    retrieved_chunks = response[0].neighbors  # List of MatchNeighbor objects
21
22    chunk_ids     = [c.datapoint.datapoint_id for c in retrieved_chunks]
23    chunk_texts   = [fetch_chunk_text(cid) for cid in chunk_ids]
24    chunk_sources = [fetch_chunk_source(cid) for cid in chunk_ids]
25
26    # Step 2: Build prompt with retrieved context
27    context_block = "
28
29".join([
30        f"[Source {i+1}: {chunk_sources[i]}]
31{chunk_texts[i]}"
32        for i in range(len(chunk_texts))
33    ])
34    prompt = f"Answer based only on these sources:
35{context_block}
36
37Question: {user_query}"
38
39    # Step 3: Generate response
40    model = GenerativeModel("gemini-2.0-flash")
41    answer = model.generate_content(prompt).text
42
43    # Step 4: Log to BigQuery — answer + exact chunk IDs used
44    bq_client = bigquery.Client()
45    audit_row = [{
46        "interaction_id":   str(uuid.uuid4()),
47        "user_id":          user_id,
48        "timestamp":        datetime.datetime.utcnow().isoformat(),
49        "query":            user_query,
50        "answer":           answer,
51        "source_chunk_ids": json.dumps(chunk_ids),    # Explainability anchor
52        "source_documents": json.dumps(chunk_sources),
53        "model":            "gemini-2.0-flash",
54        "region":           "europe-west1",
55    }]
56    bq_client.insert_rows_json(bq_audit_table, audit_row)
57
58    return {
59        "answer":  answer,
60        "sources": [{"id": chunk_ids[i], "document": chunk_sources[i]} for i in range(len(chunk_ids))],
61        "interaction_id": audit_row[0]["interaction_id"],  # Reference for future explanation requests
62    }

Requirement 4: Immutable Audit Trail in BigQuery

NIS2 and the EU AI Act both require audit logs that cannot be altered or deleted by the same parties who operate the system. BigQuery's native append-only streaming insert, combined with a table-level DML restriction via IAM, creates an immutable audit trail. Set up a dedicated service account for the runtime with insert-only permissions, and a separate account for auditors with read-only access:

bash
1# Create audit log dataset in EU with 7-year retention (NIS2 requirement)
2bq mk \
3  --dataset \
4  --location=EU \
5  --default_table_expiration=0 \
6  --description="Immutable AI system audit log — NIS2 + EU AI Act compliance" \
7  your-project:ai_audit_log
8
9# Grant runtime service account INSERT only — no UPDATE, no DELETE
10bq add-iam-policy-binding \
11  --member="serviceAccount:ai-runtime@your-project.iam.gserviceaccount.com" \
12  --role="roles/bigquery.dataEditor" \
13  your-project:ai_audit_log
14
15# Remove DELETE permission explicitly (dataEditor includes it by default)
16# Use a custom role instead:
17gcloud iam roles create aiAuditWriter \
18  --project=your-project \
19  --permissions=bigquery.tables.getData,bigquery.tables.updateData,bigquery.tables.create
20
21# Auditor account: read-only across the audit dataset
22bq add-iam-policy-binding \
23  --member="serviceAccount:compliance-auditor@your-project.iam.gserviceaccount.com" \
24  --role="roles/bigquery.dataViewer" \
25  your-project:ai_audit_log

Requirement 5: Consent Verification Before Retrieval

If your RAG system retrieves documents containing personal data about the querying user (e.g., a customer service agent that retrieves a customer's own order history), you need a valid legal basis under GDPR Article 6. For B2B systems where employees query internal documents, a legitimate interest assessment typically suffices. For consumer-facing systems, explicit consent must be verified before retrieval. Add a consent gate in the tool layer:

python
1from google.cloud import bigquery
2import datetime
3
4def check_consent_before_retrieval(user_id: str, data_category: str) -> bool:
5    """
6    Verifies GDPR consent is valid before allowing personal data retrieval.
7    Returns True only if active, non-expired consent exists.
8    """
9    bq_client = bigquery.Client()
10
11    query = """
12    SELECT consent_given, consent_expires_at, legal_basis
13    FROM `project.gdpr.user_consents`
14    WHERE user_id = @user_id
15      AND data_category = @category
16      AND consent_given = TRUE
17      AND (consent_expires_at IS NULL OR consent_expires_at > CURRENT_TIMESTAMP())
18    ORDER BY consent_given_at DESC
19    LIMIT 1
20    """
21    job_config = bigquery.QueryJobConfig(query_parameters=[
22        bigquery.ScalarQueryParameter("user_id", "STRING", user_id),
23        bigquery.ScalarQueryParameter("category", "STRING", data_category),
24    ])
25    rows = list(bq_client.query(query, job_config=job_config).result())
26    return len(rows) > 0
27
28# Usage in RAG tool
29def retrieve_personal_documents(user_id: str, query: str) -> str:
30    if not check_consent_before_retrieval(user_id, "personal_documents"):
31        # Log the blocked attempt
32        log_blocked_retrieval(user_id, query, reason="no_valid_consent")
33        return "I cannot access personal documents without valid consent on file."
34    return run_vector_search(query, filter_user_id=user_id)
Maximum EU AI Act fine (global annual turnover) for non-compliance
3%
Maximum GDPR fine (global annual turnover) per violation
4%
NIS2-required audit log retention period
7yr
When compliance architecture must be in place — not post-MVP
Day 1

The Compliance Architecture at a Glance

User Request
  ↓
Consent Gate (BigQuery consent table lookup)
  ↓ Consent valid
Vector Search (Vertex AI, europe-west1 only)
  ↓ Retrieved chunk IDs + texts
Chunk Audit: source_chunk_ids logged to BigQuery
  ↓
Gemini API (europe-west1 endpoint)
  ↓ Response generated
Full Interaction Log → BigQuery audit table (append-only, EU region)
  ├── interaction_id, user_id, timestamp
  ├── query, answer
  ├── source_chunk_ids (explainability anchor)
  └── model, region, latency_ms

Erasure Pipeline (on-demand):
  Right to Erasure Request
  → Find chunk_ids for user_id in BigQuery
  → Remove from Vertex AI Vector Search index
  → Soft-delete chunk text + embedding in BigQuery
  → Log erasure event to GDPR audit log

Compliance is not a feature you add to a RAG system at the end. Data residency locks must be set before the first row is written. The vector index must be provisioned in the correct region from day one. The audit schema must exist before the first model call. These are irreversible architectural decisions — getting them wrong requires a full rebuild, not a patch. If you are building a RAG system for a European enterprise, this architecture is the minimum viable starting point.

Request a discovery call

30 minutesGoogle MeetFree