Agentseteu.agentset.aiPlan dated 24 Sep 2026

EU Data Residency  Launch Plan

Agentset will run a second, fully separate copy of the platform for EU customers: the EU cell. It is built from the same repository and the same commits as the US stack, but it has its own accounts, database, keys and storage, hosted in Frankfurt, with AI inference in the Azure EU Data Zone. EU users and organizations sign up separately, and nothing is replicated between regions. The US stack keeps running as it does today; it only picks up the shared code changes. This page lists what the promise is, the services and keys to create (in order), where every value goes, the code work, and the checks to run before launch.

Status: vendor research and code audit are done. Nothing is provisioned yet and no code has changed.
Hub regionFrankfurtAWS eu-central-1 · Vercel fra1
AI inferenceAzure EU Data Zoneswedencentral · DataZoneStandard
Launch promiseTier 1All customer content in the EU/EEA
Work20 setup steps · 58 code items38 code items block launch

What we promise at launch

Tier 1
TIER 1 · EU DATA RESIDENCY

All customer content is stored and processed in the EU/EEA.

That covers uploaded files, extracted text, chunks, embeddings and vectors, prompts and completions, rerank inputs, and uploaded hosting assets (logos, OG images, extracted document images).

Two location details are stated up front. Azure AI inference runs in Microsoft's EU Data Zone, which follows the EU Data Boundary and can include EFTA countries such as Norway and Switzerland. Parsing containers run in Modal's broad EU region, which is the EEA.

Processing outside the EU/EEA is limited to the metadata and operations processors listed here. Each one is named in the EU sub-processor list with its transfer mechanism.

What we will not claim. "EU sovereignty", or that no data of any kind leaves the EU. Most vendors are US companies, several control planes are US-hosted, and billing, sign-in and email metadata are processed in the US.
Customer-directed transfers are outside our promise and are documented as such: outbound webhooks go to whatever URL the customer configures; bring-your-own Pinecone, Turbopuffer or Azure runs in the customer's chosen region; file ingests by URL are fetched from wherever the customer hosts them.
Tier 2 (strict) is a later phase, only if a contract requires it. See Phase 5.

Residual non-EU processors

ProcessorWhat it sees for the EU cellWhereTransfer basis
Vercel control plane & logsProject settings and env vars, customer hosting-domain list, deployment and build metadata, runtime logs and request metadata. Function code runs in fra1.US / globalDPF + SCCs (DPA)
Trigger.dev run metadataRun IDs, tags, status, logs and job completion data (counts and batch keys only, no file names). Payloads carry IDs only after the payload refactor. Workers run in eu-central-1.AWS us-east-1UK entity; SCCs / UK IDTA
Modal control plane & logsFunction-call metadata, logs, spawn inputs (IDs only after the refactor), deploy images, secrets. Request bodies are kept under Modal's 2 MiB in-region limit. Containers run in the EEA.USSCCs (DPA)
Resend account emailRecipient, subject, message body and delivery logs of account emails (sign-in codes and links, invites, billing and webhook notices). Sends from Ireland; no document content.USDPF + SCCs
Stripe billingBilling contact, payment method, invoices, subscription state, page counts.USDPF + SCCs
GitHub / Google sign-inUser-initiated OAuth: profile name, email, avatar.USDPF
Discord ops alertsInternal alerts with IDs, slugs, plan names and counts only. No personal data.USNo customer personal data sent
Datalab account planeAccount, dashboard and billing. Document processing runs in the Netherlands.USDPA (Team plan)
Cloudflare / Vercel edgeTLS termination and transit for visitors outside the EU (they hit the region nearest to them). Edge caching only for public logos and OG images; the extracted-image domain follows decision D7.GlobalDPF + SCCs
Vendor account planesAccount emails, resource names and usage for Azure, Supabase, Upstash, Turbopuffer, Tinybird, PostHog, ZeroEntropy. Turbopuffer namespace and attribute names go to its US sub-processors.Mostly USEach vendor's DPA
Agentset support & engineeringRemote access to EU systems for support and operations, limited and logged.Wherever staff are locatedAgentset DPA / SCCs

Architecture of the EU cell

same repo · separate resources

Frankfurt hub AWS eu-central-1

  • Vercel functionsfra1 · new EU project
  • Supabase Postgreseu-central-1 · new project
  • Upstash Rediseu-central-1 · no read regions
  • Turbopufferaws-eu-central-1 · EU org
  • Trigger.dev workerseu-central-1 · new project
  • TinybirdAWS eu-central-1 workspace
  • PostHog EU CloudFrankfurt
  • ZeroEntropy EUeu-api.zeroentropy.dev

Everything the app reads or writes per request sits in one AWS region, so database and vector round trips stay short.

Azure AI Services swedencentral

  • gpt-5.5 · gpt-4.1DataZoneStandard
  • gpt-5-mini · gpt-5-nanoDataZoneStandard
  • text-embedding-3-largeDataZoneStandard · 3072-d
  • Cohere rerank v4.0 pro / fastFoundry · DataZoneStandard

Data Zone processing can run in any region of the EU Data Zone, which can include EFTA countries such as Norway and Switzerland; data at rest stays in Sweden. Global deployment types are blocked by policy.

Elsewhere in the EU

  • Cloudflare R2EU jurisdiction · 3 buckets
  • Modal parsing serviceregion eu (EEA) · routing eu-west
  • Datalabprocessing_location=eu · NL

Outside the EU metadata & operations only

Control planes, logs, billing, sign-in, email and alerts. See the residual processor list.

Ingest path
  1. Dashboard / API
  2. Vercel fra1
  3. Trigger.dev worker (FRA)
  4. Modal eu
  5. Datalab NL
  6. Chunks → R2 EU + Redis EU
  7. Azure DZ embeddings
  8. Turbopuffer FRA
Search and chat path
  1. API / hosted page
  2. Vercel fra1
  3. Azure DZ query embedding
  4. Turbopuffer FRA
  5. Cohere rerank (Azure DZ)
  6. gpt-5.5 (Azure DZ)
  7. Response

Visitors outside the EU are TLS-terminated at the Vercel region nearest to them, and Next.js routing middleware runs there too, before the request reaches fra1. After workstream L4 (a launch blocker) the middleware only routes: hosting records and their access lists are cached in fra1, not in the edge region. What remains is transit, not storage, and it is disclosed.

US today vs EU cell

ComponentUS todayEU cell
Web app & API functionsVercel, iad1 (plus 8 routes pinned to iad1 in code)New Vercel project, fra1; no route pins
Dashboard / API / CNAME hostsapp.agentset.ai · api.agentset.ai · cname.agentset.aieu.agentset.ai · api.eu.agentset.ai · cname.eu.agentset.ai (Option A)
PostgresSupabase, US EastSupabase eu-central-1, new project in the existing org
RedisUpstashUpstash eu-central-1, no read regions
Managed vector storeTurbopuffer aws-us-east-1 (API default); Pinecone (dashboard "Managed" option)Turbopuffer aws-eu-central-1, separate org; no managed Pinecone
LLMsAzure OpenAI, East US 2Azure AI Services swedencentral, DataZoneStandard; openai:gpt-5 / 5.1 / 5.2 not offered
Embeddingstext-embedding-3-large (Azure)Same model, DataZoneStandard, Sweden Central
Default rerankerCohere API (api.cohere.com)Cohere rerank v4.0 pro / fast on Azure Foundry, Data Zone; v3.x not offered
ZeroEntropy rerankerapi.zeroentropy.deveu-api.zeroentropy.dev (EU account)
Background jobsTrigger.dev CloudNew Trigger.dev project, worker region eu-central-1
Parsing serviceModal, default routingSeparate Modal environment, region eu (EEA), routing eu-west
PDF / Office parsingDatalab, team default locationSeparate Datalab team, processing_location=eu (Netherlands)
Object storageCloudflare R2 (account endpoint)R2 EU-jurisdiction buckets on the .eu. endpoint
Webhook delivery logsTinybird AWS us-eastTinybird AWS eu-central-1
Product analyticsPostHog US CloudPostHog EU Cloud (or off)
EmailResend, agentset.ai senderResend eu-west-1 sending domain; metadata still stored in the US
BillingStripeSame Stripe account; EU webhook endpoint, signing secret and restricted key
Sign-inGitHub + Google OAuthNew GitHub OAuth app "Agentset EU"; new Google Web client in the same project
Ops alertsDiscordFour new EU channels, IDs only
Web crawl / YouTube ingestFirecrawl; YouTube API + proxyOff at launch (kill-switch), revisit later

Decisions needed

7 open · defaults recommended

The architecture above is settled. These seven choices are still open, and each has a recommended default. D1 must be final before any account is created, because the hostname is written into OAuth callbacks, the Stripe endpoint, CORS and auth settings. D7 must be final before the first EU ingest. D2–D6 can start on their defaults.

D1

Hostnames

decide first
Recommended: Option A. Dashboard at eu.agentset.ai, API at api.eu.agentset.ai, customer hosting CNAME target cname.eu.agentset.ai.

Option A needs a small refactor: apps/web/src/lib/constants.ts builds app.<SHORT_DOMAIN> and api.<SHORT_DOMAIN> today, so a bare eu.agentset.ai would fall through to the custom-domain middleware. New NEXT_PUBLIC_APP_HOSTNAME / NEXT_PUBLIC_API_HOSTNAME fall back to the current logic, so the US is unchanged. Shortest URL, matches what the landing page promises.

Option B needs no code: set NEXT_PUBLIC_APP_SHORT_DOMAIN=eu.agentset.ai to get app.eu.agentset.ai + api.eu.agentset.ai, and redirect eu.agentset.ai to app.eu (308, Vercel domain redirect). Faster, but a longer dashboard URL.

Either way, the DNS records are DNS-only (grey cloud) to Vercel: Cloudflare Universal SSL doesn't cover two-level subdomains when proxied.

D2

Residency tier

Recommended: Tier 1 at launch. All content in the EU; disclosed US metadata processors.

Strict (Tier 2) removes the US metadata processors, but it means self-hosting Trigger.dev, an EU-native email provider, Cloudflare's Data Localization Suite, an Azure EU Data Boundary tenant and Vercel Enterprise. That is weeks of work plus ongoing operations cost. Do it only when a contract requires it.

D3

Who can use the EU region at first

Recommended: unadvertised self-serve. Sign-up and Pro checkout work for anyone with the URL, but nothing promotes it: the landing page's EU card routes to "talk to us". Announce publicly after Phase 3 has passed and one billing cycle has run cleanly. No extra code.

True invite-only (only approved people can sign up) needs a small sign-up allowlist on EU (workstream R7, S). Advertised self-serve from day one gives the widest reach, but puts a new stack under unknown load and support volume immediately. The landing page already presents EU residency as an Enterprise feature, so the unadvertised launch matches existing messaging.

D4

Bring-your-own US embedding providers on EU

Recommended: block at launch. BYO OpenAI, Voyage and Google embeddings are rejected on EU with a message pointing to BYO Azure (EU region) or the managed default.

Allow with a warning ("processed outside the EU") keeps flexibility, but those SDKs always call US or global endpoints, so chunk text would leave the EU at the customer's request. That would need extra UI and makes the residency story harder to explain. Blocking can be relaxed later; the reverse is harder.

D5

Moving existing US customers to EU

Recommended: out of scope for launch. Customers who want EU create a new EU org and re-ingest.

An org export/import tool (sized L) would have to copy the org graph, R2 objects and vectors, rewrite asset URLs, move custom domains between Vercel projects, and duplicate identities for users who stay in US orgs too. Build it when there is demand.

D6

EU staging

Recommended: no EU previews at launch. The EU Vercel project skips non-main builds with an Ignored Build Step; previews run only on US staging.

A full EU staging stack (its own EU database, Redis, Trigger environment, hostnames and OAuth callbacks) catches region-specific breakage earlier but roughly doubles the provisioning. Add it after launch.

D7

Extracted-image URLs

lock before first EU ingest
Recommended: public custom domain eu-files.agentset.ai (same model as US), with a Cloudflare Cache Rule that bypasses the edge cache for that hostname. Images are then stored only in the EU bucket; non-EU visitors get TLS transit at the edge, as for every other hostname. No code.

Authenticated EU image proxy (images private, served through an access-checked route in fra1) is the strongest option and what the storage research recommends long-term, but it needs code (M) and changes how chunk text and API responses reference images. Public with edge caching (exactly like US) is simplest, but then the promise must say that extracted images served from a public URL may be cached at Cloudflare edge locations outside the EU. Image URLs are written into chunk text at ingest, so this can't change cheaply later.

Services & accounts to create

in setup order

Cards 1–19 only provision and collect values; nothing is deployed until card 20, which runs the first EU deploy in a fixed order. Work through them in order: later services need values from earlier ones. Hostnames below assume D1 = Option A. Placeholders in angle brackets come from the vendor's console.

0 of 20 done
Values go to:Vercel EUTrigger EUModal EU secretCloudflare DNSGitHub secrets (CI)Local / password manager
1

Vercel — EU project & domains (no deploy yet)

functions fra1Pro plansame teamcontrol plane & logs US

How to get it

  1. Create the project from the CLI without importing Git (a dashboard import deploys straight away, and the build would fail without the later cards' values):
    vercel project add <eu-project>, then cd apps/web && vercel link --yes --project <eu-project>. Git is connected in card 20.
  2. Settings → Domains: add eu.agentset.ai and api.eu.agentset.ai (and cname.eu.agentset.ai) as the first step. Copy the CNAME target each domain card shows (Vercel now issues project-specific targets).
    CLI: vercel domains add eu.agentset.ai <eu-project>, and the same for the other two.
  3. Settings → Build and Deployment: Root Directory apps/web, Framework Next.js, and copy any build/install command overrides from the US project.
  4. Settings → Functions → Function Regions: untick iad1, tick fra1, save. Keep Fluid compute on; match the US Function CPU setting.
  5. Settings → Environment Variables: turn on "Automatically expose System Environment Variables". Add Vercel EU values from the matrix as later cards produce them; card 20 checks the list is complete.
  6. API token: switch to your personal account → vercel.com/account/tokens → scope: the team (All Projects) → expiry 1 year → create and copy (shown once). It must be team-scoped: the app calls the team-level GET /v6/domains/{domain}/config.
  7. IDs: Project ID from the EU project's Settings → General; Team ID from Team Settings → General (same as US).
  8. Firewall: recreate the US project's custom rules and rate limits (they're per project and not copied).
  9. Leave Observability Plus, Web Analytics and Speed Insights off, and don't attach log drains that ship EU logs to US tools.
2

Cloudflare DNS — EU hostnames

agentset.ai zoneDNS only (grey cloud)no content

How to get it

  1. dash.cloudflare.com → agentset.ai zone → DNS → Records → Add record. Every record below: Proxy status DNS only, TTL Auto.
  2. eu → CNAME → the target from the eu.agentset.ai Vercel domain card.
  3. api.eu → CNAME → the target from the api.eu.agentset.ai domain card.
  4. cname.eu → CNAME → cname.vercel-dns.com (same as the US cname record). The dashboard shows customers cname.<SHORT_DOMAIN> as their hosting CNAME target.
  5. Option B instead: app.eu and api.eu to the Vercel targets, and eu for the redirect domain.
  6. If the zone has CAA records, make sure 0 issue "letsencrypt.org" is allowed, or Vercel can't issue certificates.
  7. Wait for "Valid Configuration" on each Vercel domain card. A _vercel TXT record is only needed if Vercel says another team uses the domain.
  8. Later steps add more records here: Resend (card 15) and R2 custom domains (card 5, created by R2 itself).
3

Supabase — EU Postgres

eu-central-1 Frankfurtexisting orgcompute: mirror US

How to get it

  1. supabase.com/dashboard → org switcher → the existing org → New project. Generate a database password and save it right away (shown once).
  2. Region: open the picker and choose Central EU (Frankfurt) eu-central-1 from the specific regions list. Do not pick the general "Europe" option: it can land in London or Zurich. The region can't be changed later.
  3. Compute: mirror the US project (Small or larger if you want PITR). If the form offers "Enable Data API", untick it: the app uses Prisma only.
    CLI: supabase projects create <name> --org-id <ORG_ID> --db-password '<PW>' --region eu-central-1 --size small (check --size values with --help).
  4. If you skipped it on the form: Project Settings → Data API → disable. Leave Realtime off.
  5. Connect → ORMs → Prisma: copy the transaction pooler URL (port 6543) as DATABASE_URL and the session pooler URL (port 5432, IPv4) as DIRECT_URL. Copy the host exactly; its index number can't be derived. Add the TLS parameters from workstream L1 (Postgres client hardening):
    DATABASE_URL=postgresql://…:6543/postgres?pgbouncer=true&sslmode=require&uselibpqcompat=true
    DIRECT_URL=postgresql://…:5432/postgres?sslmode=require&uselibpqcompat=true
    With node-postgres 8, a bare sslmode=require is treated as verify-full and fails against Supabase's CA, so keep uselibpqcompat=true (or use verify-full with the Supabase CA if L1 pins it).
  6. Database → Settings → SSL Configuration: download the CA (needed only if L1 pins it). Order: L1 merged → URLs with TLS parameters → test (card 20's migration status check and a sign-in) → then turn on Enforce SSL.
  7. The schema is applied in card 20 from a git-ignored .env.eu.local. Optionally dry-run all 45 migrations against a throwaway EU project now.
  8. Database → Backups: daily backups are on (7 days on Pro); add the PITR add-on if US has it.
  9. Keep the org's AI assistant data sharing off and don't use the dashboard AI on this project. Never add a read replica outside the EU.
  10. Legal: the DPA is accepted with the ToS; download it and the TIA PDF, and subscribe to sub-processor notices.
4

Upstash — EU Redis

primary eu-central-1no read regionseviction off

How to get it

  1. Optional: Account → Teams → create an EU team so EU access is separate.
  2. console.upstash.com → Redis → Create Database → provider AWS → Primary Region Frankfurt (eu-central-1) → Read Regions: leave empty (any read region replicates all data there) → Next → plan (pay-as-you-go, or the same fixed plan as US) → Create. Don't create legacy "Regional" databases (deprecated).
    CLI: upstash redis create --name <name> --region eu-central-1 (no --read-regions).
  3. Keep eviction disabled: parsed chunk batches must survive until the job reads them. Optionally turn on daily backups.
  4. REST API section: copy the REST URL and the standard token (not the read-only one).
  5. Connect / TCP section: copy endpoint, port 6379 and password for the parsing service (TLS required).
  6. Create it directly in Upstash, not through the Vercel Marketplace: the Marketplace injects its own REDIS_URL, which clashes with the app's meaning of that name.
  7. Register at trust.upstash.com for sub-processor notices.
5

Cloudflare R2 — EU buckets

jurisdiction eu3 buckets.eu. endpoint

How to get it

  1. Check npx wrangler whoami is logged in to the Cloudflare account that owns the agentset.ai zone and the current buckets.
  2. Create three buckets with the EU jurisdiction (it can't be changed later): uploads + chunks (private), extracted images, hosting assets. Dashboard: R2 → Create bucket → Location: Specify jurisdiction → European Union.
    npx wrangler r2 bucket create <eu-uploads> --jurisdiction eu
    npx wrangler r2 bucket create <eu-images>  --jurisdiction eu
    npx wrangler r2 bucket create <eu-assets>  --jurisdiction eu
    npx wrangler r2 bucket list --jurisdiction eu
  3. CORS on the uploads bucket (the dashboard uploads with a direct browser PUT): allow PUT, GET, HEAD from https://eu.agentset.ai, header Content-Type, expose ETag. Easiest: dump the US policy with wrangler r2 bucket cors list, change the origin, then npx wrangler r2 bucket cors set <eu-uploads> --file cors.json --jurisdiction eu.
  4. Assets custom domain (public logos and OG images), a first-level name so Universal SSL covers it:
    npx wrangler r2 bucket domain add <eu-assets> --domain eu-assets.agentset.ai --zone-id <zone id> --jurisdiction eu
    Keep r2.dev off.
  5. Images bucket: follow decision D7, and hold this step until D7 is final (image URLs are written into chunk text at ingest). With the default (D7 = public, no edge cache):
    npx wrangler r2 bucket domain add <eu-images> --domain eu-files.agentset.ai --zone-id <zone id> --jurisdiction eu
    then add a Cloudflare Cache Rule for eu-files.agentset.ai that bypasses cache. With the proxy option, add no domain and keep the bucket private.
  6. Token: R2 → Manage R2 API Tokens → Create Account API token → Object Read & Write → apply to the 3 EU buckets only → copy Access Key ID and Secret (shown once). Use one token for the app and the parsing service, or create two.
  7. Optional: npx wrangler r2 bucket lifecycle add <eu-uploads> abort-mpu --abort-multipart-days 7 --jurisdiction eu
6

Azure AI Services — LLMs & embeddings

swedencentralkind AIServices · S0DataZoneStandard only

How to get it

  1. az login, then select the existing subscription (a new one starts on a lower quota tier).
  2. Dedicated resource group and resource. Kind AIServices is required so the same resource can also host the Cohere rerankers.
    az group create --name <eu-rg> --location swedencentral
    az cognitiveservices account create --name <eu-resource> \
      --resource-group <eu-rg> --location swedencentral \
      --kind AIServices --sku S0 --custom-domain <eu-resource> --yes
  3. Before any deployment, deny Global deployment types on the resource group:
    az policy definition create --name deny-global-deployments --mode All \
      --rules '{"if":{"allOf":[{"field":"type","equals":"Microsoft.CognitiveServices/accounts/deployments"},{"field":"Microsoft.CognitiveServices/accounts/deployments/sku.name","in":["GlobalStandard","GlobalProvisionedManaged","GlobalBatch","DeveloperTier"]}]},"then":{"effect":"deny"}}'
    az policy assignment create --name deny-global-deployments \
      --resource-group <eu-rg> --policy deny-global-deployments
  4. Deploy with the same deployment names as US, SKU DataZoneStandard:
    az cognitiveservices account deployment create -g <eu-rg> -n <eu-resource> \
      --deployment-name gpt-5.5 --model-format OpenAI --model-name gpt-5.5 \
      --model-version 2026-04-24 --sku-name DataZoneStandard --sku-capacity <K TPM>
    Repeat for gpt-4.1 (2025-04-14), gpt-5-mini (2025-08-07), gpt-5-nano (2025-08-07) and text-embedding-3-large (version 1, 3072 dims, same model as US so demo embeddings stay valid). Don't create gpt-5 / 5.1 / 5.2 chat deployments: EU hides those options.
  5. Set each deployment's version upgrade option explicitly (for example no auto-upgrade) so a model change is always a reviewed change. Check the versionUpgradeOption property in the deployment list.
  6. Portal alternative: ai.azure.com → the resource → Models + endpoints → + Deploy model → Deploy base model → Customize → Deployment type Data Zone Standard → exact name. Never pick Global Standard or Global Batch.
  7. Key: az cognitiveservices account keys list -n <eu-resource> -g <eu-rg> --query key1 -o tsv (or Keys and Endpoint → KEY 1).
  8. Verify SKUs: az cognitiveservices account deployment list -n <eu-resource> -g <eu-rg> --query "[].{name:name,model:properties.model.name,sku:sku.name}" -o table — every row DataZoneStandard.
  9. Smoke test from an EU machine: POST https://<eu-resource>.openai.azure.com/openai/v1/responses with "store": false, and the /embeddings?api-version=preview endpoint.
  10. Quota: EU Data Zone quota starts lower than US. Check az cognitiveservices usage list -l swedencentral and request more at aka.ms/oai/stuquotarequest (region swedencentral, Data Zone Standard) before launch.
  11. Optional: ask your Microsoft contact about modified abuse monitoring (only for Microsoft-managed customers or eligible programs).
7

Cohere rerank on Azure Foundry

same resourceCohere-rerank-v4.0-pro / -fastData Zone Standard

How to get it

  1. ai.azure.com → the EU resource/project → Model catalog → search Cohere-rerank-v4.0-proDeploy.
  2. In the dialog change Deployment type to Data Zone Standard (Global Standard may be preselected; the deny policy will reject it anyway). Keep the name exactly Cohere-rerank-v4.0-pro. Repeat for Cohere-rerank-v4.0-fast.
  3. Build the endpoint by hand (the portal's "Get endpoint" has been reported wrong for Cohere): https://<eu-resource>.services.ai.azure.com/providers/cohere. The SDK appends v2/rerank.
  4. Smoke test with the resource key (API-key auth; Entra tokens have returned 500s on this route). model must equal the deployment name, including case:
    curl -X POST https://<eu-resource>.services.ai.azure.com/providers/cohere/v2/rerank \
      -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
      -d '{"model":"Cohere-rerank-v4.0-pro","query":"capital of France",
           "documents":["Paris is the capital of France","Berlin is in Germany"],"top_n":2}'
    If Bearer is rejected, retry with header api-key: $KEY.
  5. Management center → Quota → Data Zone Standard: request more rerank quota before launch (low defaults reported).
8

Turbopuffer — EU organization

aws-eu-central-1separate orgLaunch or Scale

How to get it

  1. turbopuffer.com/dashboard → create a separate organization for EU. API keys are org-wide and valid in every region, so a separate org keeps EU keys, audit trail and billing apart. If the org switcher doesn't offer it, ask Turbopuffer support (the path isn't documented).
  2. Pick a plan. Default: Launch ($16/mo minimum; all public regions, the SOC 2 report and the DPA) unless SSO or audit logs are contractually required, in which case Scale (adds SSO, audit logs, IP allowlisting and a BAA).
  3. Create a read/write API key and copy it (shown once).
  4. No namespace setup is needed: namespaces are created on first write to the regional host. Smoke test against https://aws-eu-central-1.turbopuffer.com/v2/namespaces/<test> (upsert, query, delete).
  5. Leave native embedding off, and never use copy_from_namespace across US and EU.
  6. Keep the SDK's own TURBOPUFFER_BASE_URL / TURBOPUFFER_REGION env vars unset.
  7. Legal: the DPA is part of the ToS; subscribe to sub-processor notices; get the SOC 2 report from the Trust Center.
9

ZeroEntropy — EU cluster

eu-api.zeroentropy.devseparate EU account

How to get it

  1. Sign up at eu-dashboard.zeroentropy.dev. It is separate from the US dashboard; US keys don't work on the EU API.
  2. Add billing → API Keys → Create key.
  3. Smoke test POST https://eu-api.zeroentropy.dev/v1/models/rerank with zerank-2, zerank-1 and zerank-1-small.
  4. Request the DPA/SCCs and sub-processor list through ZeroEntropy's trust center, and get written confirmation that logs, telemetry and support access for the EU cluster stay in the EU.
10

Datalab — EU team

processing_location=eu · NLTeam planaccount plane US

How to get it

  1. Sign in at datalab.to and create a separate team for EU (team settings are under Settings → Team; the exact menu path wasn't confirmed). This keeps billing, keys and the residency default separate.
  2. Subscribe the EU team to the Team plan (needed for the DPA), then ask Datalab support to execute the DPA.
  3. Email Datalab support to set the EU team's residency default to EU. The code also sends processing_location=eu on every request.
  4. In team settings, make sure the data-retention consent (model-training opt-in) is off.
  5. Create an API key at www.datalab.to/app/keys with a spend limit.
  6. Test: POST https://www.datalab.to/api/v1/convert with file_url = an EU R2 presigned URL, mode=balanced, processing_location=eu; poll request_check_url; download result_url. EU mode rejects multipart uploads.
  7. Ask in writing: where the API front door and request metadata are processed; that no US LLMs are used for EU requests (captions, chart understanding, accurate mode); retention of files fetched by URL; the EU sub-processor list.
  8. Never use Datalab's public Playground with customer documents.
11

Modal — EU parsing service

region eurouting eu-west (Dublin)separate environment

How to get it

  1. Land the parsing-service code changes first: region-parameterised deploy, ID-only spawn inputs, ID-only logging, Datalab EU, the crawl/YouTube kill-switch and minimal completion data (workstream P1–P6).
  2. In the parsing-service repo: uv sync, then confirm uv run modal profile current is the Agentset workspace.
  3. Create a dedicated environment named eu in the existing workspace (secrets are scoped per environment):
    uv run modal environment create eu
  4. Create the EU secret in that environment with one command (each --force create replaces the whole secret). AGENTSET_REGION must be in it: the kill-switch reads it inside the container at runtime.
    uv run modal secret create --env eu --force <secret-name> \
      AGENTSET_REGION=eu \
      AGENTSET_API_KEY=<new random> DATALAB_API_KEY=<EU team key> \
      DATALAB_PROCESSING_LOCATION=eu \
      REDIS_HOST=<EU Upstash> REDIS_PORT=6379 REDIS_PASSWORD=<...> \
      R2_ENDPOINT_URL=https://<ACCOUNT_ID>.eu.r2.cloudflarestorage.com \
      R2_ACCESS_KEY_ID=<...> R2_SECRET_ACCESS_KEY=<...> \
      R2_CHUNKS_BUCKET_NAME=<eu-uploads> R2_BUCKET_NAME=<eu-images> \
      R2_PUBLIC_URL=<per D7, e.g. https://eu-files.agentset.ai>
  5. Deploy with the region selector. AGENTSET_REGION picks the app name, secret and regions (every function region="eu", the web function routing_region="eu-west"); MODAL_ENVIRONMENT picks the environment. Never deploy EU into the default environment. Routing region can only be set on a function's first deploy, so the EU app gets its own name.
    AGENTSET_REGION=eu MODAL_ENVIRONMENT=eu uv run modal deploy -m src.app
  6. Copy the web endpoint URL from the dashboard (environment → Apps → web function). Use the base URL only: the jobs append /ingest.
  7. Verify that MODAL_REGION read at container start is an EU region, and that the Usage page shows the region multiplier.
  8. For CI deploys: Settings → API Tokens → New token.
  9. Legal: the DPA (SCCs) is incorporated by reference; subscribe to sub-processor notices at trust.modal.com and request the SOC 2 report.
12

Trigger.dev — EU project

workers eu-central-1Pro planrun metadata us-east-1

How to get it

  1. cloud.trigger.dev → project switcher → New project. Default: the existing org (one Pro plan). Switch to a separate org, which needs its own paid plan, only if EU runs must not share concurrency or member visibility with US.
    CLI: npx trigger.dev@latest projects create --org <org-slug> --name <eu-project>
  2. Project sidebar → Regions → set eu-central-1 (Europe, Frankfurt) as the default. Check with npx trigger.dev@latest projects get <proj_ref>.
  3. Project settings → copy the proj_… ref into packages/jobs/.env.eu.local as TRIGGER_PROJECT_ID (the .local suffix keeps it git-ignored).
  4. Production environment → API keys → New API key → copy tr_prod_… to the EU Vercel project as TRIGGER_SECRET_KEY.
  5. The Regions page lists the eu-central-1 static egress IPs (paid plans). Only enable Supabase network restrictions if every client has fixed egress; Vercel functions and the migration runner don't by default, so otherwise leave restrictions off.
  6. For CI: Profile → Personal Access TokensTRIGGER_ACCESS_TOKEN.
  7. Task env vars, the deploy and the smoke test happen in card 20, once every value exists. Don't add HTTP, Prisma or OpenAI OpenTelemetry instrumentations to the EU config.
13

Tinybird — EU workspace

AWS eu-central-1Forward workspace

How to get it

  1. Install the Tinybird Forward CLI (tb) following Tinybird's docs. Use a Forward workspace: Classic has been sunset for Free/Developer plans.
  2. Log in to the EU region: tb login --host https://api.eu-central-1.aws.tinybird.co. Don't commit the local .tinyb file.
  3. tb workspace create <eu-workspace> then tb workspace use <eu-workspace> (or UI: workspace switcher → Create workspace → region AWS eu-central-1).
  4. Add TOKEN "agentset_tinybird_token" READ to packages/tinybird/pipes/get_webhook_events.pipe, so one least-privilege token can append and read.
  5. Deploy: cd packages/tinybird && tb --cloud deploy --check, then tb --cloud deploy.
  6. Copy the resource-scoped token (tb --cloud token ls --match agentset). Never put the workspace admin token in app env.
  7. Leave Tinybird's AI assistants unused on this workspace; review the DPA at tinybird.co/dpa.
14

PostHog — EU Cloud project

eu.posthog.com · Frankfurtseparate account

How to get it

  1. Sign up at eu.posthog.com/signup (a separate instance from US; the same Google/GitHub login works).
  2. Create an organization and a project for eu.agentset.ai.
  3. Project settings → General: copy the Project API key (phc_…).
  4. Organization settings: keep AI data processing / PostHog AI off.
  5. Project settings: session replay off (or on with input and text masking); autocapture masked; add https://eu.agentset.ai to authorized URLs.
  6. Sign the DPA with PostHog's self-serve generator.
  7. To launch with no product analytics at all, leave NEXT_PUBLIC_POSTHOG_KEY unset: the app skips analytics without it.
15

Resend — EU sending domain

eu-west-1 Irelandmail.eu.agentset.aimetadata stored US

How to get it

  1. Domains → Add Domainmail.eu.agentset.ai → Region Ireland (eu-west-1). The region can't be edited later (delete and re-add). A dedicated subdomain per region is what Resend recommends, and it keeps mail records away from the eu CNAME.
    CLI: resend domains create --name mail.eu.agentset.ai --region eu-west-1
  2. Add exactly the records Resend shows in Cloudflare DNS (DNS only): DKIM TXT, and MX plus SPF TXT on the return-path subdomain. Optionally DMARC. Then Verify.
  3. API Keys → Create API Key → Permission Sending access → Domain mail.eu.agentset.ai. A domain-restricted key refuses other senders, which is a useful guard.
  4. Settings → Documents: download the DPA.
16

Stripe — EU endpoint & key

one shared accountUS processor

How to get it

  1. Ship the region filter to the US stack first (workstream B1). Every endpoint on an account receives every event; each stack must answer the other region's events with a 200 and ignore them.
  2. Rehearse on a local dev run (NODE_ENV=development, so the test prices are used) with sandbox keys and the EU env, forwarding sandbox events to localhost:<port>/api/stripe/webhook with the Stripe CLI. A deployed production build would pick the live prices.
  3. Workbench → Webhooks → Create an event destination → Your account → API version = the SDK's pinned version (2025-02-24.acacia) → the same events as the US endpoint (checkout.session.completed, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed, invoice.paid) → Webhook endpoint → URL https://eu.agentset.ai/api/stripe/webhook → Create → Reveal secret.
  4. Right after copying the secret, disable the endpoint (from the endpoint's page, or the API's disabled=true). It would otherwise receive every US billing event while the EU app isn't deployed yet. Card 20 re-enables it after the first EU production deploy.
  5. Developers → API keys → Create restricted key: Checkout Sessions write, Customer portal write, Customers write, Subscriptions write, Products read, Prices read, PaymentMethods read, Invoices read, Billing meter events write. Use the sandbox request logs to find any missing permission (403).
  6. Reuse the account's publishable key, products, prices and the ingested_pages meter. The customer portal needs no change.
  7. Optional for EU business customers: tax ID collection or Stripe Tax for reverse-charge VAT invoices.
17

GitHub — "Agentset EU" OAuth app

org-ownedexact-match callback

How to get it

  1. github.com/organizations/agentset-ai/settings/applications (Org → Settings → Developer settings → OAuth Apps) → New OAuth App.
  2. Name Agentset EU; Homepage https://eu.agentset.ai; callback https://eu.agentset.ai/api/auth/callback/github. Leave wildcard matching and Device Flow off.
  3. Register application → copy the Client ID → Generate a new client secret and copy it at once (shown once).
  4. Store both in the EU Vercel project; sign-in is tested in card 20.
18

Google — new Web OAuth client

same Google Cloud projectno re-verification

How to get it

  1. console.cloud.google.com → the project that owns the current production client → Google Auth Platform → Clients.
  2. Branding: check that Authorized domains includes agentset.ai (it covers eu.agentset.ai). Don't edit branding: any change triggers brand re-verification.
  3. Clients → Create client → Web application → name it for the EU → Authorized redirect URI https://eu.agentset.ai/api/auth/callback/google → Create.
  4. Copy the Client ID and secret immediately: the secret is only shown at creation.
  5. Store both in the EU Vercel project; sign-in is tested in card 20 (new redirect URIs can take a few minutes to work).
19

Discord — four EU alert channels

IDs onlyUS service

How to get it

  1. In the internal server, create #eu-alerts, #eu-cron, #eu-subscribers, #eu-errors, restricted to the people who need them.
  2. For each: Edit Channel → Integrations → Webhooks → New Webhook → Copy Webhook URL. Treat the URLs as secrets.
  3. Policy: alerts carry IDs, slugs, plan names, event types and counts only. No emails, names, document names, queries or raw customer error payloads.
20

First EU deploy

after cards 1–19fixed ordergate: launch blockers merged

How to do it

  1. Gate. Every launch blocker in Region foundation, AI engine, Jobs, Partition API, Data layer, Billing and Analytics & alerts, plus C1–C2, is merged to main and deployed to US. Deploying with fewer changes would still send EU data to the US without any error. For example, without E1 the managed vector store is hard-coded to aws-us-east-1 and would accept the EU organization's key. The R4 guards refuse to start when E1/E4/E5 settings are missing.
  2. Migrate the EU database from the git-ignored .env.eu.local (DIRECT_URL with TLS parameters):
    cd packages/db
    bun run db:deploy:eu
    bun run db:status:eu
  3. Confirm the EU parsing service (card 11) was deployed from the same commit.
  4. Trigger.dev EU → Environment Variables → Production: add every Trigger EU value from the matrix (● and ◐). Nothing syncs automatically, and several packages validate env at import, so a missing value crashes every task.
    CLI: TRIGGER_PROJECT_ID=<eu ref> npx trigger.dev@latest env set KEY value --secret
  5. Deploy jobs: cd packages/jobs && bun run trigger:deploy:eu (reads ./.env.eu.local, runs trigger deploy --skip-sync-env-vars).
  6. Complete the Vercel EU env: every ● and ◐ value in the matrix's Vercel column (vercel env ls production in the linked apps/web). DATABASE_URL must be there at build time (Prisma generate runs on install).
  7. Connect Git: vercel git connect, then Settings → Git → Ignored Build Step: skip non-main branches (D6), and turn on Deployment Protection for previews.
  8. Production deploy: push to main (or redeploy from the dashboard). The build must pass env validation.
  9. Verify region: Runtime Logs → open a request → the Function location must read fra1 for every route, including chat and search.
  10. Re-enable the Stripe EU endpoint (disabled in card 16).
  11. Smoke tests: GitHub and Google sign-in on the EU host; ingest a PDF and a text document (the Trigger run page shows eu-central-1 and an IDs-only payload); search and chat; a Stripe checkout (EU endpoint 200, US endpoint 200 "other region"). Then turn on Supabase Enforce SSL (card 3) and continue with Phase 3.

Not provisioned for the EU at launch

  • Managed Pinecone: no EU index. Env vars become optional and MANAGED_PINECONE is rejected on EU; customers can still bring their own EU Pinecone index.
  • Cohere API direct (api.cohere.com): no EU endpoint and no Cohere dashboard key needed; replaced by the Azure Foundry deployments.
  • Firecrawl, YouTube Data API and the transcript proxy: crawl and YouTube ingestion are off on EU (kill-switch); revisit later.
  • Phase 2 only: self-hosted Trigger.dev, EU-native email, Cloudflare Data Localization Suite, Azure EU Data Boundary tenant, Vercel Enterprise EU-only failover.

Master env-var matrix

EU values only

Every variable the EU cell needs, from the five env.ts schemas (web, engine, storage, Stripe, emails), turbo.json, the parsing service's secret, and the new variables introduced by the code changes. Values in angle brackets come from the named console. Hostname values assume D1 = Option A.

required must be set (validation or guard), value unused here optional, may be unset not usednewoptional on EU
VariableEU value / sourceVercel EUTrigger EUModal EUNotes
Region & hostnames
Values assume D1 = Option A. For Option B: BETTER_AUTH_URL and APP_DOMAIN = https://app.eu.agentset.ai; leave NEXT_PUBLIC_APP_HOSTNAME / NEXT_PUBLIC_API_HOSTNAME unset; use app.eu.agentset.ai in the GitHub/Google callbacks, the Stripe endpoint URL, the R2 CORS origin and the PostHog authorized URLs.
NEXT_PUBLIC_DEPLOYMENT_REGION neweuus on the US stack. Drives model and provider allowlists, Stripe region metadata and startup guards. Must differ.
NEXT_PUBLIC_APP_SHORT_DOMAINeu.agentset.aiBuild-time. Also yields cname.eu.agentset.ai. The default silently falls back to agentset.ai, so set it. Must differ.
NEXT_PUBLIC_APP_HOSTNAME neweu.agentset.aiOption A only. Unset on US (falls back to app.<SHORT_DOMAIN>).
NEXT_PUBLIC_API_HOSTNAME newapi.eu.agentset.aiOption A only.
APP_DOMAIN requiredhttps://eu.agentset.aiExists as optional in packages/emails; becomes required on EU (R1). Email links and List-Unsubscribe. Must be added to turbo.json globalEnv.
NEXT_PUBLIC_VERCEL_ENVVercel system variableTurn on "Automatically expose System Environment Variables", or links fall back to localhost.
NEXT_PUBLIC_APP_NAMEunset (defaults to Agentset)Shared with US is fine.
Auth
BETTER_AUTH_URLhttps://eu.agentset.aiExact app origin, no trailing slash (checked against the request origin).
BETTER_AUTH_SECRETopenssl rand -base64 32Must be unique per region. Never copy the US value.
GITHUB_CLIENT_ID
GITHUB_CLIENT_SECRET
<from "Agentset EU" OAuth app>Must differ.
GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET
<from new Web client>Must differ. Same Google Cloud project.
Database & Redis
DATABASE_URL<Supabase transaction pooler :6543>?pgbouncer=true&sslmode=require&uselibpqcompat=trueMust differ. TLS per L1; test before turning on Enforce SSL. Also needed at build time (Prisma generate).
DIRECT_URL<Supabase session pooler :5432>?sslmode=require&uselibpqcompat=trueMigrations only: .env.eu.local or a GitHub secret for the migrate workflow. Same TLS parameters; check with db:status:eu.
DATABASE_CA_CERT new<PEM from Database → Settings → SSL Configuration>Only if L1 pins the Supabase CA (verify-full) instead of the URL parameters above.
REDIS_URL<Upstash REST URL>Same database as the Modal TCP credentials. Must differ.
REDIS_TOKEN<Upstash REST token, not read-only>
REDIS_HOST
REDIS_PORT
REDIS_PASSWORD
<Upstash TCP endpoint> / 6379 / <password>Parsing service writes chunk batches here.
AI providers
DEFAULT_AZURE_RESOURCE_NAME<EU resource subdomain>Must differ. The startup guard rejects the US resource.
DEFAULT_AZURE_API_KEY<KEY 1 of the EU resource>Jobs embed chunks; web embeds queries and runs LLMs.
DEFAULT_COHERE_BASE_URL newhttps://<eu-resource>.services.ai.azure.com/providers/cohereUnset on US (SDK default api.cohere.com). Required on EU: the R4 guard refuses to start without it, in web and in every job (jobs never rerank).
DEFAULT_COHERE_API_KEY<Azure resource KEY 1>On EU this holds the Foundry key; a Cohere dashboard key is not needed. Jobs: required by the engine env import until J6.
DEFAULT_ZEROENTROPY_API_KEY<key from eu-dashboard.zeroentropy.dev>US keys don't work on the EU API. Must differ. Jobs: required by the engine env import until J6.
DEFAULT_ZEROENTROPY_BASE_URL newhttps://eu-api.zeroentropy.dev/v1Passed explicitly so the SDK never falls back to the US API. Checked by the R4 guard in every job.
DEFAULT_TURBOPUFFER_API_KEY<EU organization key>Must differ (separate org).
DEFAULT_TURBOPUFFER_REGION newaws-eu-central-1Defaults to aws-us-east-1 so US is unchanged. Never change after launch.
DEFAULT_PINECONE_API_KEY
DEFAULT_PINECONE_HOST
unsetoptional on EU after E2 (required by env validation until then); managed Pinecone is rejected on EU.
SECONDARY_PINECONE_API_KEY
SECONDARY_PINECONE_HOST
unsetoptional on EU same as above.
Parsing service link
PARTITION_API_URL<EU Modal web endpoint, base URL, no /ingest>Jobs call it. Web: build-time env validation, and R4 checks it is the EU URL. Must differ.
PARTITION_API_KEYopenssl rand -hex 32Same value as AGENTSET_API_KEY in the Modal secret. Must differ from US. Web: build-time env validation.
Object storage (R2)
S3_ENDPOINThttps://<ACCOUNT_ID>.eu.r2.cloudflarestorage.comThe guard asserts .eu.. The non-EU endpoint can't see EU buckets.
S3_ACCESS_KEY
S3_SECRET_KEY
<R2 token scoped to the 3 EU buckets>Must differ.
S3_BUCKET<EU uploads bucket>Must equal R2_CHUNKS_BUCKET_NAME.
IMAGES_S3_BUCKET<EU images bucket>Must equal R2_BUCKET_NAME.
ASSETS_S3_BUCKET<EU assets bucket>
ASSETS_S3_URLhttps://eu-assets.agentset.aiAlso feeds the next/image allowlist once R1 derives it from this variable.
Jobs (Trigger.dev)
TRIGGER_SECRET_KEY<tr_prod_ key of the EU project>Must differ: a US key would run EU jobs in the US project.
TRIGGER_PROJECT_ID<proj_ ref of the EU project>Deploy time only: packages/jobs/.env.eu.local (git-ignored) or a GitHub secret.
TRIGGER_ACCESS_TOKEN<tr_pat_ personal access token>GitHub secret, only for CI deploys.
TRIGGER_API_URLunset (Tier 1)Only for a self-hosted EU Trigger.dev (Tier 2). Defaults to https://api.trigger.dev.
Billing
STRIPE_API_KEY<EU restricted rk_live_ key>Separate key so it can be revoked on its own.
NEXT_PUBLIC_STRIPE_PUBLIC_KEY<account pk_live_ key>Shared with US is fine. Jobs: required by the Stripe env import until J6.
STRIPE_WEBHOOK_SECRET<whsec_ of the EU endpoint>Must differ.
Email
RESEND_API_KEY<sending-only key restricted to mail.eu.agentset.ai>Jobs send the webhook-failure emails.
EMAIL_FROM_DOMAIN newmail.eu.agentset.aiDefaults to agentset.ai on US.
Analytics, logs & alerts
NEXT_PUBLIC_POSTHOG_KEY<EU project phc_ key> or unsetUnset disables product analytics.
NEXT_PUBLIC_POSTHOG_HOST newhttps://eu.i.posthog.comDefault is the US host. The UI host is derived from it.
NEXT_PUBLIC_POSTHOG_ASSETS_HOST newhttps://eu-assets.i.posthog.comUsed by the /_proxy rewrites.
TINYBIRD_API_URLhttps://api.eu-central-1.aws.tinybird.coMust differ.
TINYBIRD_API_KEY<resource-scoped token>Append + read on the one datasource and pipe.
DISCORD_HOOK_ALERTS
DISCORD_HOOK_CRON
DISCORD_HOOK_SUBSCRIBERS
DISCORD_HOOK_ERRORS
<EU channel webhook URLs>Optional; must differ from US channels.
Vercel API (customer domains)
VERCEL_PROJECT_ID<EU project prj_ ID>Must differ: customer domains attach to the EU project.
VERCEL_TEAM_ID<same team_ ID>Shared with US is fine.
VERCEL_API_TOKEN<new team-scoped token>Separate token, stored as Sensitive.
Parsing service (Modal EU secret & deploy)
AGENTSET_REGION neweuDeploy time (shell/CI): selects the app name, secret name and region="eu" / routing_region="eu-west"; the environment comes from MODAL_ENVIRONMENT. Runtime too (kill-switch), so it must also be in the EU secret. Unset keeps US behaviour.
MODAL_ENVIRONMENTeuDeploy time (shell/CI) only. Selects the Modal environment for deploy and secret lookup; never deploy EU into the default environment.
AGENTSET_API_KEY= PARTITION_API_KEYChecked on every request.
DATALAB_API_KEY<EU Datalab team key>Must differ (separate team).
DATALAB_PROCESSING_LOCATION neweuSent on every Datalab request. Unset on US.
R2_ENDPOINT_URLhttps://<ACCOUNT_ID>.eu.r2.cloudflarestorage.comSame as S3_ENDPOINT.
R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY
<EU-scoped R2 token>
R2_CHUNKS_BUCKET_NAME= S3_BUCKETMismatch shows up as "Document JSON not found".
R2_BUCKET_NAME= IMAGES_S3_BUCKET
R2_PUBLIC_URLper D7 (default https://eu-files.agentset.ai)Final before the first EU ingest: written into chunk text.
FIRECRAWL_API_KEYunset (crawl off)optional on EU after the kill-switch change.
YOUTUBE_API_KEY
PROXY_USERNAME
PROXY_PASSWORD
unset (YouTube off)optional on EU after the kill-switch change.
MODAL_TOKEN_ID
MODAL_TOKEN_SECRET
<workspace API token>GitHub secrets, only for CI deploys.
Tooling (optional)
NEXT_TELEMETRY_DISABLED1Keeps Next.js build telemetry off the processor list.
CHECKPOINT_DISABLE1Prisma CLI telemetry, wherever EU migrations run.
SKIP_ENV_VALIDATIONnever set on EUValidation is the first line of defence against a missing EU value.

All new variables, plus APP_DOMAIN (required on EU after R1), go into turbo.json globalEnv. When the build runs through Turbo, its strict env mode hides undeclared variables from the task, and a shared remote cache must not reuse a US-configured build.

Code workstreams

58 items · 38 launch blockers

Deduplicated across the eight audits (for example, the preferredRegion pins were flagged four times and appear once here). Effort: S ≈ under a day, M ≈ a few days, L ≈ a week or more. The shared changes also ship to the US stack. Env-gated changes default to today's US behaviour; the rows that change a shared payload or US behaviour say so and give the rollout order.

PR #139 (oRPC + hosted MCP, unmerged) adds two more preferredRegion = "iad1" pins (v1/[...rest]/route.ts:17 for the whole REST API and [transport]/route.ts:9 for MCP), hard-coded api.agentset.ai in the MCP instructions (lib/mcp/index.ts:22) and a single-server spec (server/orpc/spec.ts). They need the same fixes as workstream items R2, C3, C4 and L2 before or right after merge, and its spec-parity fixture must be updated on purpose.

Region foundation make the same build behave as US or EU

IDChangeEffortLaunch
R1Deployment region flag and fail-fast config

NEXT_PUBLIC_DEPLOYMENT_REGION; on EU require the hostnames and APP_DOMAIN; fail the build when VERCEL=1 but NEXT_PUBLIC_VERCEL_ENV is unset; assert BETTER_AUTH_URL equals the app origin; add every new var and APP_DOMAIN to turbo.json; derive the next/image remotePatterns hostname from ASSETS_S3_URL (default assets.agentset.ai).

apps/web/src/env.ts · apps/web/src/lib/constants.ts · apps/web/src/lib/auth.ts · apps/web/next.config.ts:15-21 · packages/utils/src/constants.ts · turbo.json

Sblocker
R2Remove per-route region pins

Delete export const preferredRegion = "iad1" from 8 routes; the region comes from each Vercel project (US iad1, EU fra1). Add a CI grep that forbids literal region codes. Same fix in PR #139. Before merging: US project → Settings → Functions → Function Regions shows iad1, and only iad1, so US routes don't move.

api/(internal-api)/chat/route.ts:40 · hosting-chat/route.ts:74 · hosting-search/route.ts:18 · (public-api)/v1/namespace/[namespaceId]/search/route.ts:17 · (public-api)/health/route.ts:5 · middleware/hosting/route.ts:6 · middleware/hosting/member/route.ts:6 · middleware/default-org/route.ts:6

Sblocker
R3Env-driven app and API hostnames (decision D1, Option A)

NEXT_PUBLIC_APP_HOSTNAME / NEXT_PUBLIC_API_HOSTNAME with the current app./api. derivation as fallback; explicit *.vercel.app mapping; table-driven tests proving eu.agentset.ai reaches the app, not the custom-domain middleware.

apps/web/src/lib/constants.ts · apps/web/src/env.ts · apps/web/src/lib/middleware/utils.ts · apps/web/src/app/sitemap.ts · apps/web/src/proxy.ts · turbo.json · .env.example · apps/web/test/constants.test.ts (new)

Mblocker
R4Residency guards

When region is EU, check at startup and at the start of every job that Azure resource, Turbopuffer region, Cohere and ZeroEntropy base URLs, S3 endpoint, Tinybird host, Redis and parsing URL are on an EU allowlist, and that implicit SDK env vars (TURBOPUFFER_BASE_URL, TURBOPUFFER_REGION, ZEROENTROPY_BASE_URL, CO_API_KEY, AZURE_RESOURCE_NAME, AZURE_API_KEY) are unset. /api/health alerts when VERCEL_REGION isn't EU.

packages/engine/src/env.ts · packages/jobs/src/db.ts · apps/web/src/app/api/(public-api)/health/route.ts

Mblocker
R5Per-region migrations and deploys

db:deploy:eu / db:status:eu (reading .env.eu.local), trigger:deploy:eu (reading packages/jobs/.env.eu.local), and an optional workflow that migrates both databases, then deploys both regions from the same commit. Ignore .env.* except .env.example.

packages/db/package.json · package.json · packages/jobs/package.json · .gitignore · .github/workflows/ (new)

Mblocker
R6Env documentation

.env.example lists every variable including the new ones, PARTITION_API_URL as a base URL, and drops the unused NEXT_PUBLIC_APP_DOMAIN.

.env.example · docs/open-source/step-1-local-setup.mdx · docs/open-source/step-4-partitioner-api.mdx

Sno
R7EU sign-up allowlist (only if D3 = invite-only)

On EU, only allow sign-up for listed emails or domains; everyone else gets a "request access" message.

apps/web/src/lib/auth.ts · apps/web/src/env.ts

Sno

AI engine providers, models, vector store

IDChangeEffortLaunch
E1Configurable managed Turbopuffer region

DEFAULT_TURBOPUFFER_REGION replaces the "aws-us-east-1" literal; validated against the region enum, which is synced with Turbopuffer's list (adds gcp-europe-west1 and others, fixes a typo).

packages/engine/src/vector-store/index.ts:62 · packages/engine/src/env.ts · packages/validation/src/vector-store/turbopuffer.ts

Sblocker
E2Turbopuffer-only stack

Pinecone env vars optional; clear error if managed Pinecone is used without them; reject MANAGED_PINECONE on EU; the dashboard's "Managed" option maps to MANAGED_TURBOPUFFER (this changes the US dashboard default on purpose, matching the API default); correct the schema description of the default.

packages/engine/src/env.ts · packages/engine/src/vector-store/index.ts · packages/validation/src/vector-store/index.ts · apps/web/src/components/create-namespace/vector-store-step.tsx · apps/web/src/services/namespaces/validate.ts

Sblocker
E3Region-aware LLM list

Hide and reject openai:gpt-5, gpt-5.1, gpt-5.2 on EU (UI, hosting config, API) with a clear 4xx instead of an Azure 404. Send store: false on every Responses call (data minimisation; a deliberate change on US too). Keep deployment names equal to real model IDs: the AI SDK infers reasoning behaviour from the name.

packages/engine/src/llm/index.ts · packages/validation/src/llm/constants.ts · packages/validation/src/llm/index.ts

Mblocker
E4Cohere via Azure Foundry

Pass DEFAULT_COHERE_BASE_URL as baseUrl; map rerank-v4.0-pro/fast to the Cohere-rerank-v4.0-* deployment names; reject v3.x on EU; agentic search's hard-coded cohere:rerank-v3.5 must use an EU-available model; log and alert rerank errors instead of silently returning unranked results; check the region before any network call.

packages/engine/src/rerank/cohere.ts · packages/engine/src/rerank/index.ts · packages/engine/src/env.ts · packages/validation/src/re-ranker/constants.ts · packages/validation/src/re-ranker/index.ts · apps/web/src/lib/agentic/search.ts:68

Mblocker
E5ZeroEntropy EU endpoint

Pass DEFAULT_ZEROENTROPY_BASE_URL explicitly (the SDK otherwise reads an implicit env var or defaults to the US API).

packages/engine/src/rerank/zeroentropy.ts · packages/engine/src/env.ts

Sblocker
E6BYO embedding policy (decision D4)

On EU, reject BYO OpenAI, Voyage and Google embeddings with a message pointing to BYO Azure or the managed default (or show a "processed outside the EU" warning if D4 goes the other way).

packages/validation/src/embedding-model/openai.ts · voyage.ts · google.ts · packages/engine/src/embedding/index.ts · apps/web/src/components/create-namespace/*

Mblocker
E7BYO Azure embedding config accepts a resource name

The main "bring your own EU model" path; accept a bare resource name or an endpoint URL and migrate stored values.

packages/validation/src/embedding-model/azure.ts · packages/engine/src/embedding/index.ts · apps/web/scripts/migrate-azure-config.ts

Sno
E8Hashed tenant segment in vendor namespace names

EU only. The EU cell starts empty, so it can use hashed tenant IDs from day one without a migration. Must ship before the first EU write: changing it later needs a re-index.

packages/engine/src/vector-store/turbopuffer/index.ts:50 · packages/engine/src/vector-store/pinecone/index.ts:35

Sblocker
E9Shorter presigned URL lifetime for parsing calls

About 3 hours covers the 2-hour job plus Datalab polling (hardening). presignGetUrl already accepts expiresIn; only the call site changes.

packages/engine/src/partition/index.ts:61

Sno

Jobs Trigger.dev payloads and side channels

IDChangeEffortLaunch
J1ID-only document job payloads

The per-document job receives {documentId, ingestJobId, cleanup} and loads its configuration from the EU database (data minimisation). US rollout: deploy jobs that accept both payload shapes first, then web; drop the old shape next release.

packages/jobs/src/schema.ts · packages/jobs/src/tasks/ingest.ts · re-ingest.ts · process-document.ts

Mblocker
J2ID-only webhook delivery payloads

Store the rendered event in the EU database, trigger with {webhookId, eventId}, and load the delivery details inside the task (data minimisation). Same for the test-webhook sender. US rollout: migration → jobs deploy whose send-webhook accepts both payload shapes → web deploy → remove the old shape next release.

packages/db/prisma (new model + migration) · packages/jobs/src/schema.ts · packages/jobs/src/tasks/send-webhook.ts · packages/webhooks/src/emit.ts · apps/web/src/server/api/routers/webhooks.ts

Mblocker
J3Non-fatal, validated webhook logging

A Tinybird failure after a successful delivery must not make the task retry and redeliver. Validate Tinybird env with createEnv and assert an EU host on EU.

packages/tinybird/src/client.ts · packages/jobs/src/tasks/send-webhook.ts

Sblocker
J4Retention and redaction for webhook delivery logs

TTL on the datasource, redacted and capped bodies, deletion on org delete (data minimisation).

packages/tinybird/datasources/agentset_webhook_events.datasource · packages/tinybird/src/webhook-events.ts · packages/jobs/src/tasks/send-webhook.ts · packages/jobs/src/tasks/delete-org.ts

Sno
J5Explicit run region

Pass region: "eu-central-1" in every trigger helper when the deployment region is EU, so a dashboard change can't move EU runs.

packages/jobs/src/schema.ts

Sno
J6Relax import-time validation of unused vars in jobs

Jobs import Stripe plans and the engine, which require the publishable key and rerank keys they never use.

packages/stripe/src/env.ts · packages/engine/src/env.ts

Sno
J7Demo corpus base URL and EU mirror

Optional: build demo file URLs from config and copy the public demo files to the EU assets bucket.

packages/demo/src/index.ts · packages/jobs/src/tasks/seed-demo-namespace.ts

Sno

Partition API repo the Modal parsing service

IDChangeEffortLaunch
P1Region-parameterised deploy

AGENTSET_REGION selects app name and secret name, region="eu" on all functions and routing_region="eu-west" on the web function, and is baked into the image env for the runtime kill-switch. MODAL_ENVIRONMENT selects the environment; a local-only guard refuses an EU deploy outside the eu environment. Unset keeps US behaviour; confirm the current production app name and ref before merging.

src/app.py · src/web_app.py · src/ingest.py · src/crawl.py · src/yt.py

Sblocker
P2ID-only spawn inputs, small requests

Modal can't .spawn() functions routed outside us-east and stores spawn inputs, and any payload over 2 MiB, in the US. The web function stores the request in EU Redis with a TTL and spawns the worker with a job ID; workers return tiny outputs; the unused /results endpoints go. Keep call_id in the response (the jobs check it). On EU the app uploads TEXT documents to the EU bucket and sends a presigned URL (as for managed files), so no request to the service carries inline document text.

src/web_app.py · src/ingest.py · src/crawl.py · src/yt.py · src/schema.py · packages/engine/src/partition/index.ts:49

Mblocker
P3ID-only logging

Log IDs and sizes only; scrub tracebacks and error strings returned to the jobs (data minimisation).

src/ingest.py · src/crawl.py · src/yt.py · src/datalab_parser.py

Sblocker
P4Datalab EU processing

Send processing_location from DATALAB_PROCESSING_LOCATION; poll request_check_url; download result_url (EU results aren't returned inline); move from the legacy /api/v1/marker to /api/v1/convert; delete results after reading.

src/datalab_parser.py

Mblocker
P5Crawl and YouTube kill-switch

The EU service rejects /crawl and /youtube; the app rejects CRAWL and YOUTUBE ingests on EU with a clear error and hides them in the UI; their keys become optional in the secret schema.

src/crawl.py · src/yt.py · src/app.py · packages/validation/src/ingest-payload/* · apps/web ingest UI

Sblocker
P6Minimal job completion data

The data sent to complete the job's waitpoint (stored by Trigger.dev in us-east-1) carries counts and batch keys only: no file names, types or URLs. Check what the document job reads from it before removing fields.

src/ingest.py · src/crawl.py · src/yt.py · src/notify_trigger.py · packages/jobs/src/tasks/process-document.ts

Sblocker
P7TTL on transient chunk batches

Expire batches in Redis; best-effort cleanup when a document job fails.

src/ingest.py · packages/jobs/src/tasks/process-document.ts

Sno
P8Deploy docs

Base URL format, one secret-create command with every key, the new vars, and an EU runbook.

README.md · .env.example

Sno
P9No runtime fetches

Bake the chunking recipes into the image; optional EU filter for the transcript proxy.

src/chunker.py · src/youtube/converter.py · src/app.py

Sno
P10Configurable Trigger callback and retry

TRIGGER_API_URL (default api.trigger.dev) for Tier 2; retry the completion call with backoff.

src/notify_trigger.py

Sno

Data layer database, keys, domains

IDChangeEffortLaunch
L1Postgres client hardening

Explicit TLS configuration (URL parameters sslmode=require&uselibpqcompat=true, or verify-full with the Supabase CA via DATABASE_CA_CERT), pool size and timeouts sized for Supavisor; declare @vercel/functions in packages/db.

packages/db/src/client.ts · packages/db/src/trigger.ts · packages/db/package.json

Sblocker
L2Region-tagged API keys

EU keys start agentset_eu_; each stack answers the other region's keys with a 401 that names the right API host. Also in PR #139's oRPC and MCP auth. Must ship before the first EU key is issued: older keys can't be routed by prefix.

apps/web/src/services/api-key/create.ts · apps/web/src/lib/api/handler/base.ts · apps/web/src/lib/api/api-key.ts · (PR #139) server/orpc/base.ts · lib/mcp/auth.ts

Sblocker
L3Cross-region custom domains

Handle custom domains that already exist on another project. Plus general domain validation.

apps/web/src/server/api/routers/domains.ts · apps/web/src/lib/domains/utils.ts

Sblocker
L4Hosting cache out of edge middleware

Middleware runs in the region nearest the visitor; move hosting-record caching into the fra1 route handler so non-EU edge regions only route and never cache hosting access lists.

apps/web/src/lib/middleware/hosting.ts · apps/web/src/app/api/(internal-api)/middleware/hosting/route.ts

Sblocker
L5Org export/import tool (decision D5)

Only if existing customers must move.

apps/web/scripts/migrate-org-to-region.ts (new)

Lno

Billing

IDChangeEffortLaunch
B1Stripe region isolation

metadata.region on checkout sessions, subscription_data and customers; the webhook route returns 200 for other-region events (missing tag counts as US); the checkout handler looks up the org first and no-ops if it isn't local. Ship to US before the EU endpoint exists.

apps/web/src/server/api/routers/billing.ts · apps/web/src/app/api/(internal-api)/stripe/webhook/route.ts · checkout-session-completed.ts · invoice-payment-succeeded.ts

Sblocker
B2Live/test detection from the key prefix

Needed before any EU staging with test keys.

packages/stripe/src/plans.ts

Sno

Analytics & alerts

IDChangeEffortLaunch
A1Env-driven PostHog hosts

NEXT_PUBLIC_POSTHOG_HOST / _ASSETS_HOST in the rewrites (plus the /array rewrite and skipTrailingSlashRedirect from PostHog's Next.js guide), the UI host and the server client; skip rewrites when no key is set.

apps/web/next.config.ts · apps/web/src/instrumentation-client.ts · apps/web/src/lib/analytics-server.ts · apps/web/src/env.ts · turbo.json

Sblocker
A2Analytics data minimisation

Events carry IDs and counts only; identify by user ID; no analytics on hosted pages; masked autocapture.

apps/web/src/app/[hostingId]/(defaultLayout)/search/use-search.ts · …/playground/search/page.client.tsx · apps/web/src/hooks/use-auth.ts · use-session.ts · …/team/invite-dialog.tsx · create-org-form.tsx · components/create-namespace/index.tsx · …/hosting/domain-card/index.tsx · instrumentation-client.ts

Sblocker
A3IDs-only alerts with a region prefix

Alerts carry IDs, slugs and counts only; region prefix ([EU] / [US]).

apps/web/src/lib/log.ts · …/stripe/webhook/checkout-session-completed.ts · customer-subscription-deleted.ts

Sblocker
A4Structured error logging

Function and job logs carry error name, status code and IDs only; LLM, rerank, embedding and vector SDK errors are logged the same way, never with the error object's request details (data minimisation).

apps/web/src/lib/api/handler/base.ts · auth.ts · public.ts · apps/web/src/lib/api/errors.ts · apps/web/src/lib/agentic-search/index.ts · packages/jobs/src/tasks/*

Sblocker
A5No third-party browser requests

Self-host model logos, generate avatars locally, self-host the image-compression worker, and replace the Cal.com embed with a link-out on EU.

packages/ui/src/components/ai-elements/model-selector.tsx · packages/ui/src/components/ui/avatar.tsx · packages/ui/src/components/image-uploader.tsx · apps/web/src/hooks/use-cal.ts · apps/web/src/lib/cal.ts

Sblocker
A6Pseudonymised visitor IPs in rate-limit keys

Hash the IP for hosted-chat limits and turn off rate-limit analytics.

apps/web/src/app/api/(internal-api)/hosting-chat/route.ts · apps/web/src/lib/api/rate-limit.ts

Sno

Customer-facing surfaces emails, SDKs, OpenAPI, docs, MCP, landing, trust

IDChangeEffortLaunch
C1Email links and sender domain

Templates take the domain from APP_DOMAIN (no app.agentset.ai defaults); the OTP email gets a domain prop; webhook added/failed/disabled emails pass the domain; EMAIL_FROM_DOMAIN drives all from-addresses, including billing emails.

packages/emails/src/env.ts · send.ts · templates/*.tsx · turbo.json · apps/web/src/lib/auth.ts:101 · apps/web/src/lib/webhook/create-webhook.ts:84 · packages/jobs/src/tasks/send-webhook.ts:117,142 · …/stripe/webhook/utils.ts:48 · checkout-session-completed.ts:133

Sblocker
C2Dashboard code snippets use the region's API host

cURL uses API_DOMAIN; EU snippets set baseUrl (TS, AI SDK) and server_url (Python).

apps/web/src/lib/code-examples/ingest.ts · playground.ts · …/playground/api-dialog.tsx · apps/web/src/openapi/v1/code-samples.ts

Sblocker
C3OpenAPI named servers

Static list, US first (x-speakeasy-server-id: us), EU second (eu), identical on both stacks. Ship to US production so the Python SDK and docs pick it up.

apps/web/src/openapi/index.ts · (PR #139) apps/web/src/server/orpc/spec.ts

Sblocker
C4MCP

@agentset/mcp: --base-url / --region flags and env vars, then release. Hosted MCP (PR #139): instructions built from API_DOMAIN, no region pin, region-aware key check.

mcp-server src/index.ts · src/utils.ts · README.md · (PR #139) [transport]/route.ts · lib/mcp/index.ts

Sblocker
C5SDK region options

TS SDK region option, env fallback and key-prefix routing; Python SDK regenerated with server='eu'. baseUrl and server_url already work, so this isn't blocking.

agentset-ts packages/agentset/src/agentset.ts · src/types/common.ts · agentset-python src/agentset/_hooks/registration.py

Sno
C6Docs: Regions page and region-aware setup pages

Hosts per region, keys and data don't cross regions, SDK/cURL/MCP configuration, hostnames to allowlist, per-region sub-processors and model availability, US-only integrations; EU login in the navbar; changelog.

docs/docs.json · docs/get-started/regions.mdx (new) · docs/api-reference/introduction.mdx · tokens.mdx · pagination.mdx · docs/get-started/quickstart.mdx · sdks.mdx · architecture.mdx · docs/production/mcp-server.mdx · hosting-ui.mdx · deployment-options.mdx · docs/data-ingestion/multimodal-input.mdx · docs/changelog.mdx

Mblocker
C7Region identity in the UI

Region badge on login and in the sidebar, cross-links between regions, API and MCP base URLs on the API keys page.

apps/web/src/app/app.agentset.ai/login/login-form.tsx · apps/web/src/components/app-sidebar/links.ts · …/settings/api-keys/*

Sno
C8Landing site

Region choice on Login and Get started; the EU residency card links to eu.agentset.ai or "talk to us" (D3); privacy policy transfers section; llms.txt.

landing-site: navbar, footer, hero, security card, privacy page, pricing tiers, public/llms.txt

Sblocker
C9Trust center

Per-region sub-processor lists with location and transfer mechanism, and a data-residency section.

trust: apps/trust/src/data/trustContent.ts · apps/trust/src/components/SubprocessorsSection.tsx

Mblocker
C10Status page and README

EU monitors and component; EU sign-up link.

README.md · status page config

Sno
C11Pipedream integration

Upstream PR adding a base-URL field; document as US-only until then.

PipedreamHQ/pipedream components/agentset/agentset.app.mjs

Sno
C12Docs API playground

Turn off Mintlify's proxy and add CORS for the docs origin, or set the playground to display only.

docs/docs.json · apps/web/src/proxy.ts or lib/middleware/api.ts

Sno

Phased timeline

phases 0 and 1 run in parallel
0

Decisions & accounts start now

  • Decide D1 first (blocking). The hostname goes into OAuth callbacks, the Stripe endpoint, R2 CORS, PostHog and auth settings; changing it later means redoing cards 1, 2, 5, 14, 16, 17 and 18. Then create the EU Vercel project and add its domains (card 1).
  • Decide D2–D7: D2–D6 can start on their defaults; D7 must be final before the first EU ingest.
  • Start the long-lead items: Datalab EU team + Team plan + residency email; Azure Data Zone quota requests (LLMs and Cohere rerank); ZeroEntropy DPA; the legal entity question (Art. 27).
  • Ask vendors in writing: Supabase and Upstash backup/log locations; Datalab front door and LLM sub-processors; Modal log location, spawn behaviour and large request bodies; Trigger.dev EU data-plane roadmap; Vercel log storage.
  • Create accounts that need nothing from code: Supabase project, Upstash database, R2 buckets, Azure resource and deployments, Turbopuffer and ZeroEntropy EU orgs, Discord channels. After D1 is final: GitHub app, Google client, PostHog EU, the Stripe endpoint and the R2 CORS rule.
1

Code foundation no EU accounts needed

  • Region foundation R1–R5 and engine E1–E6 and E8. Env-gated changes default to today's US behaviour and merge safely; E2's dashboard default and E3's response-storage setting change US behaviour on purpose. Before merging R2: US project → Settings → Functions → Function Regions shows iad1 (and only iad1).
  • Jobs J1–J3 and partition service P1–P6. J1 and J2 change shared job payloads: follow the rollout order in their rows (jobs accepting both shapes first).
  • Billing B1 ships to US first, before the EU webhook endpoint is enabled.
  • Data layer L1–L4, analytics & alerts A1–A5, emails C1, snippets C2, OpenAPI C3 (to US production).
  • Lock before the first EU ingest or first EU API key: E8 namespace hashing, L2 key prefix, D7 image URLs and DEFAULT_TURBOPUFFER_REGION. None of these can change cheaply once EU data exists.
  • Coordinate with PR #139: same pin, host, spec and key-prefix fixes.
2

Provision & wire the EU stack

  • Work through cards 1–19 in order, filling the matrix as you go. They provision only.
  • Gate: every launch blocker needed at runtime (Region foundation, AI engine, Jobs, Partition API, Data layer, Billing, Analytics & alerts, plus C1–C2) is merged and deployed to US before the first EU deploy of web, jobs or the parsing service.
  • Then run card 20, First EU deploy, in its exact order: migrate → parsing service → Trigger env and deploy → Vercel env, Git and deploy → fra1 check → Stripe endpoint on → smoke tests.
  • From now on every release migrates both databases, then deploys both regions from the same commit.
3

Verification 0 of 0 passed

4

Legal, docs & launch

  • EU customer DPA (Art. 28) with 2021 SCCs Modules 2 and 3, the UK Addendum and Annex II security measures. It covers remote support and engineering access by staff outside the EU (limited, logged, stated).
  • EU sub-processor list: entity, country, purpose, data categories, processing region and transfer mechanism per vendor, with change notifications.
  • Art. 27 EU representative if there is no EU entity (and a UK representative if selling to the UK). Transfer impact assessments for SCC-based transfers, including staff access from outside the EU; Art. 30 record for the EU stack; DPIA if marketed for sensitive data.
  • Privacy policy transfers section; trust center data-residency page (C9); docs Regions page (C6); landing page (C8).
  • OpenAPI and SDKs: with C3 live on US production (Speakeasy pulls the spec from there, so it must be identical on both stacks), run the Python SDK's Speakeasy generation workflow (manual dispatch) and check its Server Selection shows server='eu'; confirm the combined "with code samples" spec used by the docs re-syncs. Release @agentset/mcp with --base-url / --region and a TS SDK minor with a region option.
  • Docs: in docs/docs.json set api.url to full, add a "Login (EU)" navbar link, and settle the API playground (proxy off with CORS for the docs origin, or display simple; C12).
  • Status page: monitors for https://api.eu.agentset.ai/health and the EU app, plus an "EU region" component.
  • EU Data Act terms for switching and export; say "EU data residency", not "EU sovereignty".
  • Unadvertised launch (D3), then announce once a billing cycle has run cleanly.
5

Strict tier optional · only if a contract demands it

  • Self-host Trigger.dev v4 in the EU (Helm or Docker; Postgres, Redis, ClickHouse, object storage and a registry). Set TRIGGER_API_URL on Vercel EU and the parsing service. No checkpoints, so waits hold compute.
  • EU-native email: Scaleway Transactional Email (fr-par) or AWS SES eu-central-1.
  • Cloudflare Data Localization Suite (Regional Services + Customer Metadata Boundary, Enterprise) for EU-only TLS and logs.
  • Azure EU Data Boundary tenant: a brand-new tenant, irreversible; existing subscriptions can't move in, so quota must be requested again.
  • Vercel Enterprise with EU-only failover regions (e.g. cdg1, arn1; not lhr1).
  • Move the parsing service off Modal to an EU container host, removing the US log and spawn-payload path.

Cost notes & open questions

list prices, Sep 2026
VendorEU vs USFixed / minimums
Vercelfra1 Active CPU $0.184/h vs $0.128 in iad1 (+44%); memory $0.0152 vs $0.0106 per GB-h; edge requests $2.60 vs $2.00 per 1MSecond project in the same Pro team: no platform fee (medium confidence)
SupabaseNo documented regional differenceEU compute billed in full if US uses the $10 credit (Micro $10, Small $15, Medium $60/mo); PITR $100/mo per 7 days
UpstashNo EU differencePay-as-you-go $0.20 per 100K commands; Prod Pack +$200/mo
Azure OpenAIData Zone about +10% vs Global (gpt-5.5 $5.50 / $33 per 1M in/out)Pay-as-you-go; EU quota starts lower than US
Cohere on FoundryPer 1,000 searches; EU Data Zone price not published staticallyCheck the Azure calculator
TurbopufferNo EU surcharge published (unit-price parity unconfirmed)Launch $16/mo min, Scale $256/mo min
ZeroEntropy$0.025 per 1M tokens, no EU differenceNone
Trigger.devNo EU surchargePro $50/mo recommended; a separate org needs its own plan
ModalRegion eu 1.15× (narrow regions 1.75×); CPU-only, so smallTeam $250/mo only if 30-day logs or custom domains are needed
DatalabEU processing 1.25× ($5 per 1K pages balanced, $12.50 accurate)Team plan $400/mo (includes $400 usage) for the DPA
Cloudflare R2No jurisdiction surchargeDLS is an Enterprise add-on (Tier 2 only)
Tinybird · PostHog · ResendNo EU differenceFree tiers likely enough; Resend Free allows 3 domains
StripeEU cards on a US account pay international and FX surchargesExtra endpoints and keys are free
LegalArt. 27 representative (annual fee), legal review of DPA, SCCs and TIAs

Open questions

Vendor confirmations (ask in writing)

  • Supabase: where backups, PITR WAL and platform logs are stored for eu-central-1 projects.
  • Upstash: backup bucket region for eu-central-1 databases.
  • Vercel: storage location and retention of runtime logs and observability; where Data Cache and Runtime Cache entries live for a fra1 project.
  • Modal: log and control-plane location for EU functions; whether web-endpoint request bodies over 2 MiB are stored in us-east; whether spawn with a non-us-east routing region is coming; DPA terms.
  • Datalab: API front-door location; LLM sub-processors for EU requests (captions, chart understanding, accurate mode); retention of files fetched by URL.
  • Turbopuffer: plan minimum for a linked EU org; per-region unit prices.
  • Azure: Cohere rerank Data Zone price and default quota.
  • ZeroEntropy: DPA/SCCs; location of logs and support access.
  • Cloudflare: custom domains on EU-jurisdiction buckets in our account; where TLS terminates for the .eu. S3 endpoint.
  • Trigger.dev: is an EU data plane on the roadmap?

Product and legal

  • Which entity contracts with EU customers, and do we need an Art. 27 representative?
  • Is Azure's EU Data Zone (which can include EFTA countries) acceptable, or will some customers need single-country processing? Regional Standard exists in Sweden only for gpt-4.1, gpt-5.1 and embeddings.
  • EUR pricing and SEPA Debit for EU customers?
  • Newer Data Zone models (gpt-5.6 and gpt-6 families) as EU substitutes for the hidden options?
  • Will PR #139 merge before the EU launch, and is hosted MCP part of the EU launch?