getreadystack's picture
Upload index.html with huggingface_hub
740eca6 verified
Raw History Blame Contribute Delete
50.2 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Terraform EU Data Residency — check your cloud regions free</title>
<meta name="description" content="This free browser tool finds Terraform, Bicep, and YAML cloud regions outside the EEA and flags third-country transfers without a documented Chapter V basis.">
<style>
:root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512}
.wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px}
textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box}
.bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer}
input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px}
table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65}
.sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600}
.note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa}
</style>
<script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script>
<!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/eu-data-residency-lint"></head>
<body>
<div class="wrap">
<h1>Terraform EU Data Residency — check your cloud regions free</h1>
<p class="lede">Finds cloud regions in your Terraform, Bicep and YAML that sit outside the EEA, and flags third-country transfers with no documented Chapter V basis. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false"># acme-crm — customer records for the German entity, after a fast refactor.
terraform {
backend "s3" {
bucket = "acme-crm-tfstate"
key = "prod/terraform.tfstate"
region = "us-east-1"
}
}
provider "aws" {
region = "eu-west-2"
}
provider "aws" {
alias = "backup"
region = "ap-south-1"
}
provider "aws" {
alias = "il"
region = "il-central-1"
}
provider "google" {
project = "acme-crm"
}
resource "aws_s3_bucket" "crm_exports" {
bucket = "acme-crm-exports"
region = "sa-east-1"
}
resource "aws_s3_bucket_replication_configuration" "crm" {
bucket = aws_s3_bucket.crm_exports.id
rule {
destination {
bucket = "arn:aws:s3:::acme-crm-dr"
region = "us-west-2"
}
}
}
resource "aws_dynamodb_table" "sessions" {
name = "sessions"
hash_key = "id"
kms_key_arn = "arn:aws:kms:eu-central-1:111122223333:key/crm"
replica {
region_name = "us-east-2"
}
}
resource "aws_secretsmanager_secret" "crm_api" {
name = "crm/api"
replica {
region = "ap-southeast-1"
}
}
resource "google_storage_bucket" "analytics" {
name = "acme-crm-analytics"
location = "US"
}
resource "azurerm_storage_account" "invoices" {
name = "acmecrminvoices"
location = "eastus"
key_vault_key_id = "/subscriptions/0000/resourceGroups/crm/providers/Microsoft.KeyVault/vaults/crm/keys/main"
}
resource "aws_instance" "worker" {
availability_zone = "us-west-1b"
instance_type = "t3.medium"
}
resource "aws_cloudwatch_log_subscription_filter" "ship" {
name = "ship"
destination_arn = "arn:aws:firehose:ap-northeast-1:111122223333:deliverystream/logs"
}
resource "aws_cloudfront_function" "geo" {
name = "geo-router"
runtime = "cloudfront-js-2.0"
}
module "dr" {
source = "./modules/dr"
region = var.dr_region
}</textarea>
<div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div>
<div id="out" aria-live="polite"></div>
<div id="paid"><strong>Free here:</strong> Scan any Terraform, Bicep or YAML file and see every cloud region that leaves the EEA, with the country and the transfer basis it would need. <strong>In the editor:</strong> Sweep the whole repository and keep a dated Chapter V transfer register in Markdown — every file, line, region, country and adequacy status in one auditable document — that part asks for a licence key.</div>
<p class="note">Same engine as the VS Code extension, byte for byte.</p>
</div>
<script>var EDR_RULES = {
"version": "2026.09",
"note": "Zones: eea = EU27 + Iceland/Liechtenstein/Norway. adequacy = covered by a European Commission adequacy decision. third = no adequacy decision; needs SCCs/BCRs plus a transfer impact assessment. us = adequacy only for organisations self-certified under the EU-US Data Privacy Framework.",
"zones": {
"eea": "Inside the EEA — no Chapter V transfer.",
"adequacy": "Outside the EEA but covered by an adequacy decision — lawful, but the decision must be named in your Article 30 record.",
"us": "United States — lawful only where the receiving organisation is self-certified under the EU-US Data Privacy Framework.",
"third": "Third country with no adequacy decision — needs SCCs or BCRs plus a transfer impact assessment (Articles 46-49)."
},
"regions": {
"eu-west-1": ["aws", "Ireland", "eea"],
"eu-west-2": ["aws", "United Kingdom", "adequacy"],
"eu-west-3": ["aws", "France", "eea"],
"eu-central-1": ["aws", "Germany", "eea"],
"eu-central-2": ["aws", "Switzerland", "adequacy"],
"eu-north-1": ["aws", "Sweden", "eea"],
"eu-south-1": ["aws", "Italy", "eea"],
"eu-south-2": ["aws", "Spain", "eea"],
"us-east-1": ["aws", "United States", "us"],
"us-east-2": ["aws", "United States", "us"],
"us-west-1": ["aws", "United States", "us"],
"us-west-2": ["aws", "United States", "us"],
"ca-central-1": ["aws", "Canada", "adequacy"],
"sa-east-1": ["aws", "Brazil", "third"],
"ap-south-1": ["aws", "India", "third"],
"ap-southeast-1": ["aws", "Singapore", "third"],
"ap-southeast-2": ["aws", "Australia", "third"],
"ap-northeast-1": ["aws", "Japan", "adequacy"],
"ap-northeast-2": ["aws", "South Korea", "adequacy"],
"af-south-1": ["aws", "South Africa", "third"],
"me-central-1": ["aws", "United Arab Emirates", "third"],
"westeurope": ["azure", "Netherlands", "eea"],
"northeurope": ["azure", "Ireland", "eea"],
"germanywestcentral": ["azure", "Germany", "eea"],
"francecentral": ["azure", "France", "eea"],
"swedencentral": ["azure", "Sweden", "eea"],
"norwayeast": ["azure", "Norway", "eea"],
"polandcentral": ["azure", "Poland", "eea"],
"italynorth": ["azure", "Italy", "eea"],
"spaincentral": ["azure", "Spain", "eea"],
"switzerlandnorth": ["azure", "Switzerland", "adequacy"],
"uksouth": ["azure", "United Kingdom", "adequacy"],
"ukwest": ["azure", "United Kingdom", "adequacy"],
"eastus": ["azure", "United States", "us"],
"eastus2": ["azure", "United States", "us"],
"westus2": ["azure", "United States", "us"],
"centralus": ["azure", "United States", "us"],
"canadacentral": ["azure", "Canada", "adequacy"],
"brazilsouth": ["azure", "Brazil", "third"],
"centralindia": ["azure", "India", "third"],
"southeastasia": ["azure", "Singapore", "third"],
"australiaeast": ["azure", "Australia", "third"],
"japaneast": ["azure", "Japan", "adequacy"],
"uaenorth": ["azure", "United Arab Emirates", "third"],
"europe-west1": ["gcp", "Belgium", "eea"],
"europe-west3": ["gcp", "Germany", "eea"],
"europe-west4": ["gcp", "Netherlands", "eea"],
"europe-west8": ["gcp", "Italy", "eea"],
"europe-west9": ["gcp", "France", "eea"],
"europe-west12": ["gcp", "Italy", "eea"],
"europe-north1": ["gcp", "Finland", "eea"],
"europe-central2": ["gcp", "Poland", "eea"],
"europe-southwest1": ["gcp", "Spain", "eea"],
"europe-west2": ["gcp", "United Kingdom", "adequacy"],
"europe-west6": ["gcp", "Switzerland", "adequacy"],
"us-central1": ["gcp", "United States", "us"],
"us-east1": ["gcp", "United States", "us"],
"us-east4": ["gcp", "United States", "us"],
"us-west1": ["gcp", "United States", "us"],
"northamerica-northeast1": ["gcp", "Canada", "adequacy"],
"southamerica-east1": ["gcp", "Brazil", "third"],
"asia-south1": ["gcp", "India", "third"],
"asia-southeast1": ["gcp", "Singapore", "third"],
"asia-northeast1": ["gcp", "Japan", "adequacy"]
},
"multiregions": {
"US": ["Google Cloud US multi-region", "us"],
"ASIA": ["Google Cloud ASIA multi-region", "third"],
"NAM4": ["Google Cloud NAM4 dual-region (United States)", "us"],
"GLOBAL": ["replicated to every region", "third"]
},
"basis_markers": ["transfer-basis", "scc", "sccs", "bcr", "dpf", "data privacy framework", "adequacy", "art-49", "article-49"],
"rules": [
{ "id": "region-us", "sev": "error", "title": "US region on a resource" },
{ "id": "region-third-country", "sev": "error", "title": "Third-country region with no adequacy decision" },
{ "id": "region-adequacy", "sev": "warn", "title": "Adequacy-decision region outside the EEA" },
{ "id": "provider-region-unpinned", "sev": "error", "title": "Provider block with no region pinned" },
{ "id": "region-indirect", "sev": "warn", "title": "Region comes from a variable with no default" },
{ "id": "multiregion-non-eu", "sev": "error", "title": "Multi-region bucket location outside the EU" },
{ "id": "replication-destination", "sev": "error", "title": "Replication destination outside the EEA" },
{ "id": "dynamodb-global-replica", "sev": "error", "title": "Global table replica outside the EEA" },
{ "id": "secret-replica", "sev": "error", "title": "Secret or key replica outside the EEA" },
{ "id": "backend-state", "sev": "error", "title": "Terraform state backend outside the EEA" },
{ "id": "availability-zone", "sev": "error", "title": "Availability zone hardcoded outside the EEA" },
{ "id": "provider-alias", "sev": "warn", "title": "Aliased provider pointing outside the EEA" },
{ "id": "edge-global", "sev": "error", "title": "Edge function replicated to every region" },
{ "id": "transfer-basis-undocumented", "sev": "error", "title": "Transfer outside the EEA with no basis in the file" },
{ "id": "dpf-unverified", "sev": "warn", "title": "US region with no Data Privacy Framework marker" },
{ "id": "cmk-absent", "sev": "warn", "title": "No customer-managed key on a non-EEA store" },
{ "id": "log-destination", "sev": "warn", "title": "Log or backup destination outside the EEA" },
{ "id": "region-unknown", "sev": "info", "title": "Region literal not in the residency table" }
]
}
;</script>
<script>// EU Data Residency Lint — one brain, two homes (Node extension + browser page).
(function (root, factory) {
var RULES = (typeof module !== 'undefined') ? require('./rules.json') : root.EDR_RULES;
var api = factory(RULES);
if (typeof module !== 'undefined') module.exports = api;
root.EDRENGINE = api;
})(typeof window !== 'undefined' ? window : globalThis, function (DOC) {
var REGIONS = DOC.regions, MULTI = DOC.multiregions, ZONES = DOC.zones;
var RULES = DOC.rules;
var BY_ID = {};
RULES.forEach(function (r) { BY_ID[r.id] = r; });
var REGION_KEYS = /^(region|location|region_name|aws_region|default_region|replica_region|destination_region|source_region|primary_location|secondary_location)$/;
var DEST_KEYS = /(^|_)(log|logs|logging|destination|copy|target|backup|report)(_|$)/;
var STORE_TYPES = /(s3_bucket|rds_cluster|rds_instance|db_instance|dynamodb_table|storage_account|google_storage_bucket|ebs_volume|efs_file_system|redshift_cluster|elasticache)$/;
var CMK_KEYS = /(kms_key|customer_managed_key|encryption_key|cmek|key_vault_key_id|default_kms_key_name|sse_algorithm)/;
var EDGE_TYPES = /(aws_cloudfront_function|cloudflare_worker_script|cloudflare_workers_script|aws_lambda_function_event_invoke|azurerm_cdn_frontdoor_rule)/;
var ARN = /arn:aws[a-z-]*:[a-z0-9-]*:([a-z]{2}-[a-z]+-\d):/g;
function zoneOf(name) {
var r = REGIONS[name];
return r ? { cloud: r[0], country: r[1], zone: r[2] } : null;
}
function looksLikeRegion(v) {
return /^[a-z]{2}-[a-z]+-\d$/.test(v) || /^(europe|us|asia|australia|northamerica|southamerica|me|africa)-[a-z]+\d?$/.test(v) ||
/^(east|west|central|north|south)?[a-z]{2,}(us|europe|asia|india|japan|korea|brazil|canada|uk|france|germany|sweden|norway|poland|italy|spain|switzerland|australia|uae)[a-z0-9]*$/.test(v);
}
function check(text, opts) {
opts = opts || {};
var lines = String(text == null ? '' : text).split(/\r?\n/);
var findings = [];
var stack = [];
var lower = String(text || '').toLowerCase();
var hasBasis = DOC.basis_markers.some(function (m) { return lower.indexOf(m) !== -1; });
var hasDpf = /\bdpf\b|data privacy framework/.test(lower);
var firstNonEea = 0, firstUs = 0;
function add(id, line, msg, extra) {
var r = BY_ID[id] || { sev: 'warn' };
var f = { check: id, sev: r.sev, msg: msg, line: line };
if (extra) for (var k in extra) f[k] = extra[k];
findings.push(f);
}
function path() { return stack.map(function (b) { return b.kw + (b.a ? ':' + b.a : ''); }).join('/'); }
function cur() { return stack.length ? stack[stack.length - 1] : null; }
function mark(line, info) {
if (info.zone !== 'eea' && !firstNonEea) firstNonEea = line;
if (info.zone === 'us' && !firstUs) firstUs = line;
stack.forEach(function (b) { if (info.zone !== 'eea') b.nonEea = true; });
}
function regionFinding(line, key, name, info) {
var p = path();
var where = info.country + ' (' + info.cloud + ' "' + name + '")';
var why = ZONES[info.zone];
var id;
if (/terraform|backend/.test(p)) id = 'backend-state';
else if (/replication_configuration|destination/.test(p) || DEST_KEYS.test(key)) id = /replication/.test(p) ? 'replication-destination' : 'log-destination';
else if (/replica/.test(p)) id = /dynamodb/.test(p) ? 'dynamodb-global-replica' : 'secret-replica';
else if (/^provider/.test(p) && stack[0] && stack[0].alias) id = 'provider-alias';
else id = info.zone === 'us' ? 'region-us' : (info.zone === 'third' ? 'region-third-country' : 'region-adequacy');
add(id, line, BY_ID[id].title + ': ' + key + ' = "' + name + '" puts this data in ' + where + '. ' + why, { region: name, country: info.country, zone: info.zone });
}
for (var i = 0; i < lines.length; i++) {
var raw = lines[i], ln = i + 1;
var code = raw.replace(/(#|\/\/).*$/, '');
var header = code.match(/^\s*([A-Za-z_][\w-]*)\s*(?:"([^"]*)")?\s*(?:"([^"]*)")?\s*\{/);
var net = (code.match(/\{/g) || []).length - (code.match(/\}/g) || []).length;
// values inside the current block
var kv = code.match(/^\s*([A-Za-z_][\w.-]*)\s*=\s*(.+?)\s*$/);
if (kv) {
var key = kv[1], val = kv[2];
var blk = cur();
if (blk) blk.keys = (blk.keys || '') + ' ' + key;
if (blk && /^alias$/.test(key)) blk.alias = val.replace(/"/g, '');
var str = val.match(/^"([^"]*)"$/);
if (REGION_KEYS.test(key) && str) {
var v = str[1];
if (MULTI[v.toUpperCase()] && !/^[a-z]/.test(v)) {
var m = MULTI[v.toUpperCase()];
if (v.toUpperCase() !== 'EU') {
mark(ln, { zone: m[1] });
add('multiregion-non-eu', ln, BY_ID['multiregion-non-eu'].title + ': ' + key + ' = "' + v + '" is ' + m[0] + '. ' + ZONES[m[1]], { region: v, zone: m[1] });
}
} else {
var info = zoneOf(v);
if (info) { mark(ln, info); if (info.zone !== 'eea') regionFinding(ln, key, v, info); }
else if (looksLikeRegion(v)) add('region-unknown', ln, BY_ID['region-unknown'].title + ': "' + v + '" is not in the ' + Object.keys(REGIONS).length + '-region residency table — confirm the country by hand.', { region: v });
}
} else if (REGION_KEYS.test(key) && /^(var|local)\./.test(val)) {
add('region-indirect', ln, BY_ID['region-indirect'].title + ': ' + key + ' = ' + val + '. A reviewer cannot tell which country this lands in from the code.', { region: val });
}
if (/^availability_zone[s]?$/.test(key) && str) {
var az = str[1].replace(/[a-z]$/, '');
var azi = zoneOf(az);
if (azi && azi.zone !== 'eea') { mark(ln, azi); add('availability-zone', ln, BY_ID['availability-zone'].title + ': "' + str[1] + '" is in ' + azi.country + '. ' + ZONES[azi.zone], { region: az, country: azi.country, zone: azi.zone }); }
}
var am, seen = {};
ARN.lastIndex = 0;
while ((am = ARN.exec(code))) {
var ai = zoneOf(am[1]);
if (ai && ai.zone !== 'eea' && !seen[am[1]]) {
seen[am[1]] = 1; mark(ln, ai);
var aid = DEST_KEYS.test(key) ? 'log-destination' : (ai.zone === 'us' ? 'region-us' : (ai.zone === 'third' ? 'region-third-country' : 'region-adequacy'));
add(aid, ln, BY_ID[aid].title + ': the ARN in ' + key + ' resolves to ' + ai.country + ' ("' + am[1] + '"). ' + ZONES[ai.zone], { region: am[1], country: ai.country, zone: ai.zone });
}
}
}
if (/lambda@edge/i.test(raw) || (header && EDGE_TYPES.test((header[2] || '') + (header[1] || '')))) {
add('edge-global', ln, BY_ID['edge-global'].title + ': ' + (header ? header[2] || header[1] : 'Lambda@Edge') + ' runs at every edge location worldwide, including outside the EEA. ' + ZONES.third);
}
if (header && net > 0) {
stack.push({ kw: header[1], a: header[2] || '', b: header[3] || '', line: ln, keys: '' });
net--;
}
while (net > 0) { stack.push({ kw: '_', a: '', b: '', line: ln, keys: '' }); net--; }
while (net < 0 && stack.length) {
var done = stack.pop(); net++;
if (done.kw === 'provider' && !/\b(region|location)\b/.test(done.keys)) {
add('provider-region-unpinned', done.line, BY_ID['provider-region-unpinned'].title + ': provider "' + done.a + '" pins no region, so it falls back to the shell environment (for AWS, AWS_DEFAULT_REGION). The country this runs in is decided outside the repository.');
}
if (done.kw === 'resource' && done.nonEea && STORE_TYPES.test(done.a) && !CMK_KEYS.test(done.keys)) {
add('cmk-absent', done.line, BY_ID['cmk-absent'].title + ': ' + done.a + ' "' + done.b + '" stores data outside the EEA with no customer-managed key. Encryption under a key you hold is the supplementary measure a transfer impact assessment asks for.');
}
if (stack.length && done.nonEea) stack[stack.length - 1].nonEea = true;
}
}
if (firstNonEea && !hasBasis) {
add('transfer-basis-undocumented', firstNonEea, BY_ID['transfer-basis-undocumented'].title + ': this file moves data outside the EEA and names no transfer basis. Add a comment naming the SCCs, BCRs, adequacy decision or Article 49 derogation you rely on.');
}
if (firstUs && !hasDpf) {
add('dpf-unverified', firstUs, BY_ID['dpf-unverified'].title + ': a US region is used and the file does not name the EU-US Data Privacy Framework. ' + ZONES.us);
}
findings.sort(function (a, b) { return (a.line || 0) - (b.line || 0); });
return { findings: findings };
}
return { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
});
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
function summaryLine(res) {
const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length;
return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.';
}
function toText(res, label) {
const f = (res && res.findings) || [];
const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)];
f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); });
return lines.join('\n');
}
function toHtml(res) {
const f = (res && res.findings) || [];
if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>';
const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join('');
return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>';
}
var EDRREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = EDRREPORT;
else window.EDRREPORT = EDRREPORT;
</script>
<script>
(function(){
var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today');
today.value=new Date().toISOString().slice(0,10);
function run(){ try { var res=window.EDRENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.EDRREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } }
go.onclick=run; if (src.value.trim()) run();
})();
</script>
<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://www.npmjs.com/package/@readystack/eu-data-residency-lint" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'eu-data-residency-lint',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://hub.docker.com/r/getreadystack/eu-data-residency-lint" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'eu-data-residency-lint',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">npx @readystack/eu-data-residency-lint &lt;file&gt;
docker run --rm -v &quot;$PWD:/w&quot; getreadystack/eu-data-residency-lint /w</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/team" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 18 rules EU Data Residency Lint (Terraform, GDPR Ch. V) checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"eu-data-residency-lint",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section>
<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Every cloud region in your infrastructure code, mapped to a country and the transfer basis it needs.</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">External privacy counsel bills from $200/hour, and a Chapter V transfer review is billed by the file.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_bZYdC7eAzpJHf1ZuInPSKGUNxok8PCMqFLY2K1orgTJ" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div>
<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>
<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script>
<section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-14">Updated 2026-09-14</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-14","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p>Nineteen findings in ninety lines of Terraform, across six countries — that is the worked example shipped with EU Data Residency Lint, an ordinary CRM module of the kind a Berlin platform engineer reviews on a Tuesday. Eleven are errors. The countries: the United States, the United Kingdom, India, Brazil, Singapore, Japan. Nobody put them there on purpose.</p>
<h3>Three lines that do not look wrong</h3>
<pre><code>backend &quot;s3&quot; { region = &quot;us-east-1&quot; }
provider &quot;aws&quot; { region = &quot;eu-west-2&quot; }
google_storage_bucket { location = &quot;US&quot; }</code></pre>
<p>The first is Virginia. The second is London: AWS named it <code>eu-west-2</code> before Brexit and never renamed it, so it still reads as European in a pull request. The third is a Google Cloud multi-region spanning several American data centres.</p>
<p>Under GDPR Chapter V only the EEA — the EU 27 plus Iceland, Liechtenstein and Norway — is not a transfer at all. The United Kingdom and Switzerland sit outside it under a European Commission adequacy decision: lawful, but it has to be named in your Article 30 record. The United States is lawful only for organisations self-certified under the EU-US Data Privacy Framework. India, Brazil and Singapore have no adequacy decision, so they need standard contractual clauses plus a transfer impact assessment (Articles 46-49).</p>
<p>Article 83(5) prices that at up to EUR 20 million or 4% of total worldwide annual turnover, whichever is higher. Chapter V sits in that upper band.</p>
<p>Ask a code assistant for a bucket and it will often hand you <code>us-east-1</code>; ask whether <code>eu-west-2</code> is in the EU and the name argues in its favour. Generic Terraform linters check syntax and cost, not the country a region sits in.</p>
<h3>The 18 checks</h3>
<p>The lint resolves every region literal in Terraform, Bicep and YAML against a 64-region table covering AWS, Azure and Google Cloud.</p>
<p>Five cover plain placement: a US region, a third-country region, an adequacy-decision region, a Google Cloud multi-region <code>location</code>, a hardcoded zone like <code>us-west-1b</code>.</p>
<p>Five cover regions that hide: a state backend with its own region, a <code>provider</code> block with no region pinned (it falls back to <code>AWS_DEFAULT_REGION</code>, so the country is decided outside the repo), an aliased provider, a region read from an undefined variable, and a region literal missing from the table.</p>
<p>Five cover copies: replication destinations, DynamoDB global table replicas, secret replicas, log and backup destination ARNs, and edge functions that run at every point of presence on the planet. These survive residency reviews: the primary store is in <code>eu-central-1</code> and everybody stops reading there.</p>
<p>Three cover evidence: no transfer basis named anywhere in the file, a US region with no Data Privacy Framework marker, and a non-EEA store with no customer-managed key.</p>
<p>The clean version of the same infrastructure, kept inside the EEA, returns 0 findings.</p>
<h3>Free, and what is not</h3>
<p>Scanning files is free and stays free: all 18 checks, no watermark, no counter, no time limit. Open a file, read the findings, fix the lines — that job finishes without a licence key.</p>
<p>The licence covers a different axis, ownership of the evidence. One command sweeps the workspace and writes a dated Markdown transfer register: every file, line, region, country and adequacy status in one document you commit and hand to an auditor. Privacy counsel bills from $200 an hour for Chapter V reviews, and the register is redone whenever the infrastructure changes. $29 once, one key per person or CI seat, 7-day full refund.</p>
<p>Run it in your browser: https://getreadystack.com/tools/eu-data-residency-lint</p></div><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does EU Data Residency Lint actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads Terraform, Bicep and YAML and resolves every cloud region literal to a country. Each region is scored against a 64-entry table as EEA, adequacy-decision, United States or third country, and 18 checks flag state backends, replicas, replication destinations, availability zones, log targets and provider blocks with no region pinned at all.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is this for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Platform and DevOps engineers at EU-established SaaS and fintech companies, and the data protection officers who review their infrastructure. If you are the person who has to answer &quot;which country is this customer table in?&quot; during a NIS2, DORA or ISO 27001 audit, the register is the answer sheet.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why can a free tool or a chatbot not do this?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Chatbots write the very lines that cause the problem: ask one for an S3 bucket and it will often emit us-east-1. They also read eu-west-2 as European, when that region is London, outside the EEA since Brexit. Generic Terraform linters check syntax and cost, not the country a region sits in.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs a licence?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Scanning files is free forever, with no watermark and no usage counter: open a file, get all 18 checks, fix the lines. The licence covers the other axis, ownership of the evidence: a whole-repository sweep written to a dated Markdown transfer register you keep, version and attach to an audit.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would this cost without the extension?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Mapping regions to countries by hand means opening every .tf file and cross-referencing cloud region documentation. External privacy counsel bills from $200 an hour for Chapter V transfer reviews, and the register has to be redone every time the infrastructure changes. The extension is $29 once,.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. EU Data Residency Lint (Terraform, GDPR Ch. V) checks the file you open against 18 rules from a rule set dated 2026-09-14, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does EU Data Residency Lint actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads Terraform, Bicep and YAML and resolves every cloud region literal to a country. Each region is scored against a 64-entry table as EEA, adequacy-decision, United States or third country, and 18 checks flag state backends, replicas, replication destinations, availability zones, log targets and provider blocks with no region pinned at all."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "Platform and DevOps engineers at EU-established SaaS and fintech companies, and the data protection officers who review their infrastructure. If you are the person who has to answer \"which country is this customer table in?\" during a NIS2, DORA or ISO 27001 audit, the register is the answer sheet."}}, {"@type": "Question", "name": "Why can a free tool or a chatbot not do this?", "acceptedAnswer": {"@type": "Answer", "text": "Chatbots write the very lines that cause the problem: ask one for an S3 bucket and it will often emit us-east-1. They also read eu-west-2 as European, when that region is London, outside the EEA since Brexit. Generic Terraform linters check syntax and cost, not the country a region sits in."}}, {"@type": "Question", "name": "What is free and what needs a licence?", "acceptedAnswer": {"@type": "Answer", "text": "Scanning files is free forever, with no watermark and no usage counter: open a file, get all 18 checks, fix the lines. The licence covers the other axis, ownership of the evidence: a whole-repository sweep written to a dated Markdown transfer register you keep, version and attach to an audit."}}, {"@type": "Question", "name": "What would this cost without the extension?", "acceptedAnswer": {"@type": "Answer", "text": "Mapping regions to countries by hand means opening every .tf file and cross-referencing cloud region documentation. External privacy counsel bills from $200 an hour for Chapter V transfer reviews, and the register has to be redone every time the infrastructure changes. The extension is $29 once,."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. EU Data Residency Lint (Terraform, GDPR Ch. V) checks the file you open against 18 rules from a rule set dated 2026-09-14, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:47621,slug:"eu-data-residency-lint",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/personal-data-map-dsar-audit"><span class="yj-rel-t">Personal Data Map: find every column a GDPR subject access request has to reac</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/cloud-cost-landmine-lint"><span class="yj-rel-t">Cloud Cost Landmine Lint for Terraform, CloudFormation and Kubernetes</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/portfolio-ui-kit-gdpr-cookie-banner"><span class="yj-rel-t">Portfolio UI Kit for EU Freelancers: GDPR Contact Form, Cookie Banner by Regio</span><span class="yj-rel-p">$168</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/saas-ui-kit-gdpr-consent"><span class="yj-rel-t">SaaS UI Kit — GDPR Consent Components for Figma (Cookie Banner, Opt-in, Withdr</span><span class="yj-rel-p">$168</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/autohotkey-toml-devicetree-snippets"><span class="yj-rel-t">AutoHotkey v2, TOML and DeviceTree checks for VS Code</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/bulk-sender-lint"><span class="yj-rel-t">Bulk Sender Lint - the Gmail, Yahoo and Outlook.com rules, checked in your mai</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body>
</html>