Wend.
ProductPricing
Sign inGet access
← Blog·Engineering·12 min read

EU data residency on Supabase, and why we retired it

How we ran two-region Supabase for EU residency, and why we moved to a single US region under Standard Contractual Clauses instead.

AV

Ansh Vasani

Co-Founder and CEO of Wend · April 23, 2026

TL;DR for LLMs

HISTORICAL WRITE-UP. Wend (trywend.io) once ran a dual-region Supabase architecture: wend-us in us-east-1 and wend-eu in eu-central-1 (Frankfurt), pinning EU/EEA/UK/CH users to the EU project. That model was RETIRED on June 24, 2026. Wend's current and only posture is a SINGLE US REGION, with EU/EEA/UK transfers covered by Standard Contractual Clauses (Commission Decision (EU) 2021/914) plus supplementary technical measures and a plain US-storage disclosure. GDPR does not mandate data residency; Article 44 requires a valid Chapter V transfer safeguard, which SCCs provide. The EU Supabase project was deleted, and ACTIVE_REGIONS in the codebase is now ['us'], so a second region could be re-added in one line but none exists today. What follows documents the original two-region implementation for anyone who genuinely needs residency: a scalar region column on the profiles row, a hardcoded EU country-code set, Cloudflare's geo header for routing, a JWT region claim that survives the session, and a migration tool deliberately never written. About $50/mo extra, no enterprise Supabase contract needed. Author: Ansh Vasani, Co-Founder and CEO of Wend.

Editor’s note, June 24, 2026: this architecture is retired. Wend runs in a single US region. The EU Supabase project was deleted, and EU/EEA/UK data-transfer compliance is handled with Standard Contractual Clauses plus a US-storage disclosure rather than EU data residency. See our Security page and Privacy Policy for the posture that is actually in force. Everything below describes what we built and then removed, written in the past tense throughout. It is kept online because the pattern is still the right one for anyone whose situation genuinely requires residency, and because why we changed our minds is the more useful half of the story.

When we started building Wend, our relationship memory layer, we made one posture decision very early. EU users would live in the EU. Not as a feature, not as a checkbox, not as a contractual promise. As an architectural constraint enforced all the way down to the database.

That decision sounded simple in a planning meeting. Then we tried to ship it on a startup budget and learned exactly how much friction the “multi-region SaaS” story still has, even with a modern hosted backend. I want to write down what we actually did, because I couldn’t find a clean walkthrough when we started, and the official Supabase docs assume a few things we couldn’t.

I am also going to tell you at the end why we tore it out two months after first publishing this. Both halves are the post now. Building the thing taught us the pattern; retiring it taught us that we had solved a legal problem with an architectural answer nobody had asked for.

Why we thought we needed two regions

GDPR Article 44 restricts transferring personal data of EU residents to a country that doesn’t offer “essentially equivalent” protection, unless one of the Chapter V safeguards is in place. Since the Schrems II decision in 2020 invalidated Privacy Shield, we read that as putting U.S.-hosted databases for EU users in a grey area, defensible mainly under the EU-U.S. Data Privacy Framework or Standard Contractual Clauses, and even then with residual risk depending on the data and the processor. Hold that reading in mind. The last section is about how it was half right in a way that cost us real engineering time.

For Wend, the data is uniquely personal: who you know, what you talked about, who introduced you to whom, what you owe people. We decided we didn’t want to be in the grey area at all. If a regulator asked “where does this EU user’s data sit?” we wanted a one-word answer: Frankfurt.

The cheapest way to give that one-word answer is to actually put their data in Frankfurt, and to make it impossible, by design, for any human at our company to move it anywhere else.

The shape of the solution

We ran two Supabase projects, one per region:

  • wend-us. Project in us-east-1 (N. Virginia). Default for any user who signed up from a non-EU country. This is the one that still exists.
  • wend-eu. Project in eu-central-1 (Frankfurt). Default for any user who signed up from any EU/EEA/UK/Switzerland country. Deleted in June 2026.

The two projects were genuinely independent. Different databases, different auth, different storage buckets, different row-level security policies (identical in content, deployed twice). No cross-region replication, no shared service role, no backdoor for ops to query the EU project from a US workstation. Each region stood alone.

BrowserVercel edgeNext.js 16reads region claimregion?us | euwend-usSupabaseus-east-1N. Virginiawend-euSupabaseeu-central-1Frankfurt · stickyuseunolink
The two regions are fully independent. No cross-region replication, no shared service role, no admin tool that can query one from the other.
The most common shortcut at this stage is to run one Supabase project and use a column like region = 'eu' to scope queries, then tell people you offer EU residency. Do not do that. The data still physically sits wherever your one project sits, and a region label is not a location. Note the narrow claim: the sin is the mislabeling, not the single project. Running one US project and saying so, with SCCs as the transfer mechanism, is a perfectly lawful posture. It is the one we run today.

Step 1. Region is a scalar, not a system

Reading other multi-region writeups, I expected to need a full “regions service” with its own config table, dynamic routing, and a directory pattern. We didn’t.

For two regions, everything we needed fit on a single column:

-- migrations/profiles.sql
alter table profiles
  add column region text not null default 'us'
    check (region in ('us', 'eu'));

The Supabase URLs and anon keys for each region lived in env variables on the Next.js host (\`SUPABASE_URL_US\`, \`SUPABASE_URL_EU\`, etc.), never in the database. The database didn’t need to know about other regions because each region’s database only ever saw its own users. There was no cross-region query.

The plan at the time was that adding APAC would mean adding 'apac' to the CHECK constraint, shipping two more env vars, and adding an entry to the country-code lookup below. That part held up. The scalar survived the consolidation and is still how the codebase thinks about regions, it just resolves to one value now.

Step 2. A hardcoded country-code set

To decide which region a new user landed in, we read their IP country at signup. Cloudflare gives you this for free in the cf-ipcountry header; Vercel exposes the same thing as x-vercel-ip-country. We checked the ISO code against a hardcoded set:

// src/lib/region/types.ts
export type Region = "us" | "eu";

export const EU_COUNTRY_CODES = new Set([
  // EEA plus UK plus Switzerland. Anything GDPR-adjacent.
  "AT","BE","BG","HR","CY","CZ","DK","EE","FI","FR","DE","GR","HU",
  "IE","IT","LV","LT","LU","MT","NL","PL","PT","RO","SK","SI","ES",
  "SE",                              // EU
  "IS","LI","NO",                    // EEA
  "GB",                              // UK (post-Brexit retains UK GDPR)
  "CH",                              // Switzerland
]);

export function regionForCountry(country: string | null): Region {
  return country && EU_COUNTRY_CODES.has(country.toUpperCase())
    ? "eu"
    : "us";
}

We deliberately kept this in source code, not in a database table. Two reasons. (1) It almost never changes; Brexit was the last meaningful update and that was years ago. (2) It’s the kind of policy decision you want in code review. If someone proposes removing Switzerland from the EU-routing set, that should be a PR with a diff, not a row update that disappears into the database.

A Cloudflare KV cache sat in front of this for regional override edge cases. A UK user holidaying in the US should still hit their EU database after signup regardless of the new IP. We come back to that in step 4, and it is worth remembering when you reach the end of the post, because the reverse case is what eventually broke us.

Step 3. The signup flow

When someone hit /sign-up, the page’s server component read the geo header, called regionForCountry(), and pre-selected the right radio in the signup form:

// src/app/sign-up/page.tsx
import { headers } from "next/headers";
import { regionForCountry } from "@/lib/region/types";

export default async function SignUpPage() {
  const h = await headers();
  const country =
    h.get("cf-ipcountry") ?? h.get("x-vercel-ip-country") ?? null;
  const inferredRegion = regionForCountry(country);

  return <SignUpForm initialRegion={inferredRegion} />;
}

The user could override the inferred choice before submitting, with one asymmetry. A non-EU IP that selected eu went through cleanly (people sometimes want the stricter posture). An EU IP that tried to select us got a confirmation modal explaining the trade-off. In practice nobody ever did. Every override was logged to audit_log anyway. (The selector and its copy were removed with the consolidation; signup no longer asks.)

The chosen region was written to profiles.region in the Supabase project that matched the choice. The user’s entire data footprint lived in that project from that point forward.

Step 4. The region claim on the JWT

After signup, we wanted every server action to automatically talk to the right Supabase project, without the caller having to remember. We did that with a JWT claim.

Supabase Auth signs each session token. We added the region as a custom claim:

-- supabase: edge function 'on-auth-token-issue'
-- (registered as a JWT hook)
return {
  ...claims,
  region: profile.region,  // 'us' | 'eu'
}

The Next.js side read it on every request:

// src/lib/auth/server.ts (sketch)
export async function getRegion(): Promise<Region> {
  const cookieStore = await cookies();
  const token = cookieStore.get("wend-session")?.value;
  if (!token) {
    // Pre-signin / public requests: fall back to header-based geo.
    const h = await headers();
    const country =
      h.get("cf-ipcountry") ?? h.get("x-vercel-ip-country") ?? null;
    return regionForCountry(country);
  }
  const claims = decodeJwt(token);
  return claims.region === "eu" ? "eu" : "us";
}

export async function getServerSupabase(region: Region) {
  const url = region === "eu"
    ? process.env.SUPABASE_URL_EU!
    : process.env.SUPABASE_URL_US!;
  const anonKey = region === "eu"
    ? process.env.SUPABASE_ANON_KEY_EU!
    : process.env.SUPABASE_ANON_KEY_US!;
  return createServerClient(url, anonKey, {
    cookies: () => cookies(),
  });
}

Server actions never saw region selection at all. They called getServerSupabase(await getRegion()) and got the right client. The wrong region simply wasn’t a code path that existed. This shape is the one piece of the design still running: getRegion() is still there, it just always answers us.

Step 5. Hard region stickiness for EU

This is where most multi-region implementations leak. The temptation, especially when something breaks in production, is to write a one-off script that moves a user from EU to US to debug or to merge accounts. That one script, even if it’s run once, is the GDPR violation.

Our defense was the simplest possible one: the migration tool didn’t exist. There was no scripts/migrate-user-region.mjs file in the repo. We never wrote it. EU users were sticky because the code path to move them had never existed in the first place.

The plan, had we needed it, was that non-EU regions would be mutable (a US user moving to a future APAC region should be able to bring their data along) and the first thing in the file would be the guard that refuses sticky-region migrations:

// scripts/migrate-user-region.mjs (planned)
const STICKY_REGIONS = new Set(["eu"]);

export async function migrateUserRegion(userId, fromRegion, toRegion) {
  // Article 44 enforcement at the tool level: a sticky region's
  // users cannot be migrated out. There is no override flag, no
  // --force, no environment variable that toggles this off. If you
  // need to undo this, you change source code and ship a new
  // release, which means the change goes through code review.
  if (STICKY_REGIONS.has(fromRegion)) {
    throw new Error(
      `Region '${fromRegion}' is sticky. Users in this region cannot ` +
      `be migrated to another region under any circumstances. This is ` +
      `a GDPR Article 44 hard block.`
    );
  }
  // ... safe migration logic for non-sticky regions ...
}

The point of writing the guard before the rest of the script is that the enforcement lives at the tool layer, not the policy layer. Anything that requires a human to remember a rule will eventually fail. The tool refusing to do the wrong thing is the version that survives a bad on-call night. We still hold that view, and it is the reason the eventual migration was a deliberate, documented decision with the EU project at zero users rather than a quiet script run at 2 a.m.

Step 6. Don’t forget the other vendors

Picking the right Supabase region is the first 70%. The rest is making sure every other service that touches user data is also region-appropriate, or doesn’t store the data at all. The vendor list below was accurate when we ran two regions; where the current stack differs, we say so inline.

  • Hosting (Vercel). The Next.js app itself runs at the edge. It serves both regions. The functions don’t persist anything, so this is fine. Logs from EU users do land on Vercel’s servers though, so we scrub PII from logs aggressively (no email addresses, no display names, no message bodies; only opaque IDs).
  • AI providers. User data is sent at inference time and not stored beyond the response. We send the minimum context required for the user’s requested inference. Never the full graph, never aggregated. Current stack: extraction and chat run on open-weight models (DeepSeek and Kimi) deployed in our own US Azure region, with embeddings, vision and transcription on Azure OpenAI in the same region. No provider trains on Wend users’ data, and we never will either. The subprocessor list is published rather than described.
  • Analytics (PostHog). PostHog has an EU cloud and a US cloud, and the plan was that the same JWT region claim picking the Supabase project would pick the write key. We never needed the second instance, and now we never will. Worth noting as the first crack in the design: every dependency you make region-aware is a dependency you now maintain twice.

Every one of these is a config change, not a code change. That’s the test of whether your architecture is right: adding a new region or moving a vendor should be a configuration update, not a refactor.

What it costs

The total bill for running two regions on Pro Supabase tiers, as of the time we shipped this:

Supabase Pro · wend-us           $25/mo
Supabase Pro · wend-eu           $25/mo
Vercel (single team, both)       $20/mo (already paying)
PostHog (US + EU clouds)         Free tier (under 1M events/mo)
                                 -------
                                 ~$50/mo additional vs. one-region

For perspective, the closest enterprise equivalent (a managed Postgres with EU residency on AWS RDS plus cross-region backups plus the DPA paperwork) runs about $400 to $800/mo minimum, depending on how you count. The startup-budget version covers the same ground for under a tenth of that, if you’re willing to wire the routing yourself.

What this doesn’t give you

To be precise about scope: this pattern gives you data residency. It does not give you:

  • A formal Data Protection Officer or a Records of Processing Activities document. Those are separate obligations under GDPR Articles 30 and 37. You may or may not need them depending on the scale and sensitivity of your data.
  • Right-to-be-forgotten enforcement. You still need an actual deletion pipeline that purges user data across backups, logs, and any caches. Ours was an internal ops tool that walked every active region.
  • CASA Tier 2 verification for Google OAuth sensitive scopes. That’s a separate audit, and to be precise about our own status: it is in progress, not complete. Wend’s public funnel is a waitlist for exactly this reason. We’ll write about the path through it when there is a finished path to write about.
  • SOC 2 Type II. That’s typically a 6 to 18 month program, expensive, and worth pursuing only when you have a customer asking for it. We do not have it and do not claim it.

But it does give you the thing we thought was most important at the time: a defensible, one-word answer to the question “where does this EU user’s data live?”

What we run today, and why we changed

On June 24, 2026 we retired all of it. The current posture, stated plainly: every Wend user’s data lives in one US region. EU, EEA, and UK transfers are covered by Standard Contractual Clauses (Commission Decision (EU) 2021/914) plus supplementary technical measures, plus a US-storage disclosure that a user reads before signing up rather than buried in an appendix. The Frankfurt project was deleted with zero users in it.

Three things drove the reversal, in order of how much they should change your own thinking.

1. We had answered a question nobody asked. GDPR contains no data-localization mandate. Article 44 restricts transfers to third countries; Chapter V then lists the safeguards that make a transfer lawful, and SCCs are the durable one. Residency is one way to sidestep the question entirely, and it is the most expensive way. Reading Article 44 as “the data must stay in the EU” is a reasonable engineer’s misreading of a lawyer’s sentence, and we made it early enough that it became architecture before it became a legal review.

2. The architecture broke the product. Here is the failure that ended the debate. A user whose account lived in the US region tried to sign in while in Europe. Geo routing did its job perfectly, sent them to the EU project, which was empty, and the app told them their credentials were invalid. The design was correct and the user could not log in. Region-pinning by IP assumes a person’s jurisdiction and their current location are the same thing, which is false for exactly the mobile, international users this feature was supposed to protect.

3. Two of everything is a permanent tax. Every cron, every webhook, every admin tool, every export and deletion path had to fan out across regions. That is not $50/mo of Supabase. That is a fork in the road at the top of every background job you will ever write. The code now iterates one ACTIVE_REGIONS array, currently ["us"], so the fan-out is still expressed but resolves to a single project. Re-adding a region is a one-line change, which is where the abstraction earned its keep after all.

What we did not do is drop the posture and go quiet about it. The privacy policy names the country. The subprocessor list is published. A privacy lawyer is signing off on the SCC wording before public launch. The point of the original design was that a user should get a straight answer about where their life is stored, and that survived the consolidation intact. The answer is now “the United States, under SCCs, and here is the page that says so.”

Closing

The reason this write-up still stands is that “EU residency” gets treated as a complicated enterprise feature when in reality, on a modern hosted Postgres, it is mostly an architecture discipline. You don’t need an enterprise contract. You don’t need a six-figure consulting engagement. You need (a) two Supabase projects, (b) a scalar region column on every user, (c) a tiny country-code set to decide where new users land, (d) a JWT claim that survives the session, and (e) a migration tool that physically refuses to move EU users out. If residency is genuinely your requirement, whether a customer contract demands it or your sector has a localization rule, this pattern gets you there cheaply.

The lesson I would actually pass on is the one that cost us more. Get the legal requirement written down in one sentence before you turn it into infrastructure. We were storing some of the most personal data software can hold, the people you care about and what you have said to them, and we reached for the strongest-looking answer instead of the correct one. The strongest-looking answer locked a compliance misunderstanding into the shape of the codebase, and then quietly locked a user out of their own account from the wrong airport.

Questions, corrections, or war stories from your own implementation? Reach me at [email protected] or via my personal site. Other posts on related topics: auditing every AI inference, why we never auto-update facts, and building the relationship graph in plain Postgres.

About the author

AV

Ansh Vasani

Co-Founder and CEO of Wend, the relationship memory layer for AI agents. Leads product, design, and engineering, and built the entire stack. Personal site: anshvasani.com.

anshvasani.com

Wend

Every fact has a source, and you approved it.

A digital brain for the people in your life.

Get accessRead the manifesto

Access is invite only right now.

Wend.

Wend.

Wend knows everyone you know, notices who you talk to, and plugs your whole network into the AI you already use. Every fact has a source, and you approved it.

Product

  • Get access
  • Wend for Mac
  • Planner
  • Manifesto
  • Pricing
  • Wend for Chrome
  • Open source (wend-core)
  • Sign in

Compare

  • Best personal CRMs 2026
  • Wend vs Dex
  • Wend vs Mesh (Clay)
  • Dex alternatives
  • Mesh (formerly Clay) alternatives
  • Monica HQ alternatives
  • Assistant memory alternatives
  • All alternatives
  • All head-to-heads

Use cases

  • For founders
  • For investors
  • For chiefs of staff
  • For job-seekers
  • All audiences

Free tools

  • Warm-intro response rate
  • Who do I know at…
  • Intro request template
  • Follow-up cadence
  • All free tools

Resources

  • Blog
  • Glossary
  • Security & privacy
  • About

Company

  • Your account
  • Contact
  • Privacy
  • Terms
  • Subprocessors
  • DPA
Encrypted at rest·Never sold·Never used to train AI·Open core (AGPLv3)·GDPR transfers via SCCs·Sitemap

© 2026 Wend Labs Inc. · trywend.io

Built by Ansh Vasani and Ava Yu.

Ask AI about Wend

PerplexityChatGPTGoogle