Agentseteu.agentset.aiPlan dated 24 Sep 2026Decisions D1–D4 applied

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. Every service that touches customer content gets its own EU instance (database, cache, storage, jobs, parsing and AI inference), hosted in Frankfurt with AI inference in the Azure EU Data Zone. Billing, sign-in and email reuse the US accounts with EU configuration; product analytics, webhook delivery logs and a few optional features are off on EU; and the vector store is always the customer's own (bring your own key). 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 set up (in order), where every value goes, the code work, and the checks to run before launch.

Status: vendor research and code audit are done; decisions D1–D4 were made on 24 Sep 2026. EU provisioning started the same day (below). No EU code has shipped and nothing is deployed.

Provisioning status as of 24 Sep 2026

Done

  • EU Vercel project created with functions in fra1 (not deployed); eu.agentset.ai, api.eu.agentset.ai and cname.eu.agentset.ai attached and verified (card 1).
  • Supabase EU project in eu-central-1 with SSL enforcement on; a TLS 1.3 connection verified (card 2).
  • R2 EU-jurisdiction buckets for uploads, assets and images, with CORS on the uploads bucket (card 4).
  • Azure Sweden Central resource with all seven DataZoneStandard deployments (gpt-5.5, gpt-4.1, gpt-5-mini, gpt-5-nano, text-embedding-3-large, Cohere rerank v4.0 pro and fast), auto-upgrade off, all smoke-tested (card 5).
  • Trigger.dev EU project created (card 6).
  • Modal environment eu created (card 7).

Remaining

  • Upstash EU database (card 3).
  • Dashboard steps: R2 API token and custom domains; Supabase Data API off; Trigger.dev default region eu-central-1 and production API key; the Azure deny-Global policy.
  • Datalab DPA confirmation (card 8) and the shared-account configuration (cards 9–14).
  • Code foundation (Phase 1), then the first EU deploy (card 15).
Hub regionFrankfurtAWS eu-central-1 · Vercel fra1
AI inferenceAzure EU Data Zoneswedencentral · DataZoneStandard
Vector storeBring your ownCustomer's Turbopuffer (EU regions) or Pinecone
Launch promiseTier 1All customer content in the EU/EEA
Work15 setup steps · 59 code items35 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, prompts and completions, rerank inputs, and uploaded hosting assets (logos, OG images, extracted document images).

Vectors (with their chunk text and metadata) are written to the customer's own vector store: Agentset runs no managed vector store on EU. EU namespaces accept Turbopuffer only in EU regions. For Pinecone the customer controls the index region, and we recommend an EU index.

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; the bring-your-own vector store region is the customer's choice (Turbopuffer limited to EU regions; Pinecone index region set by the customer); bring-your-own 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). Same account and sender domain as the US stack; no document content.USDPF + SCCs
Stripe billingBilling contact, payment method, invoices, subscription state, page counts. Same account as the US stack.USDPF + SCCs
GitHub / Google sign-inUser-initiated OAuth: profile name, email, avatar. Same OAuth app and client as the US stack, with the EU callback added.USDPF
Discord ops alerts (optional)Internal alerts with an [EU] tag and IDs, slugs, plan names and counts only. No personal data.USNo customer personal data sent
Datalab account planeAccount, dashboard and billing of the account shared with the US stack. EU documents are uploaded to Datalab's EU storage and processed in the Netherlands on every request.USDPA (coverage of EU processing confirmed in writing)
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 and Upstash.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
  • Trigger.dev workerseu-central-1 · new project

Functions, database, cache and job workers sit in one AWS region, so database round trips stay short. Vector latency depends on the region of the customer's vector store; aws-eu-central-1 is the closest.

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
  • DatalabEU upload + processing_location=eu · NL

Customer's own vector store bring your own key

  • Turbopuffer (customer account)EU regions only: aws-eu-central-1, aws-eu-west-1, gcp-europe-west3, gcp-europe-west1
  • Pinecone (customer account)index region set by the customer · EU recommended

Agentset runs no managed vector store on EU. The customer's vector store credentials are kept in the EU database.

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. Customer's vector store (BYOK)
Search and chat path
  1. API / hosted page
  2. Vercel fra1
  3. Azure DZ query embedding
  4. Customer's vector store (BYOK)
  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, decided)
PostgresSupabase, US EastSupabase eu-central-1, new project in the existing org
RedisUpstashUpstash eu-central-1, no read regions
Vector storeManaged Turbopuffer aws-us-east-1 (API default) or managed Pinecone (dashboard "Managed" option); bring-your-own also allowedBring your own only: the customer's Turbopuffer (EU regions only) or Pinecone (customer-chosen index region, EU recommended). No managed store.
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.devNot offered on EU (zerank models hidden)
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 locationSame Datalab account; each file is uploaded to Datalab's EU storage and converted with processing_location=eu (Netherlands), then deleted
Object storageCloudflare R2 (account endpoint)R2 EU-jurisdiction buckets on the .eu. endpoint
Webhook delivery logsTinybird AWS us-eastNot kept on EU; the dashboard's delivery log says "not available in this region"
Product analyticsPostHog US CloudNone: PostHog is not loaded on EU
EmailResend, agentset.ai senderSame Resend account and sender domain; links point to eu.agentset.ai; metadata stored in the US
BillingStripeSame Stripe account; EU webhook endpoint, signing secret and restricted key
Sign-inGitHub + Google OAuthSame GitHub OAuth app and Google web client, with the EU callback URLs added
Ops alertsDiscordOptional: the same webhooks with an [EU] tag and IDs-only messages, or off
Web crawl / YouTube ingestFirecrawl; YouTube API + proxyDisabled on EU (kill-switch), revisit later
Demo templatesSeeded into the managed vector storeDisabled on EU (no managed vector store)
Browser assets & schedulingModel logos, avatars and the image-compression worker from third-party hosts; Cal.com embedServed from the app itself; Cal.com becomes a plain link

Decisions

D1–D4 decided 24 Sep 2026 · D5–D7 on defaults

D1–D4, the scope of the EU stack and the vector-store model were decided on 24 Sep 2026. D5–D7 are still open and each has a recommended default; D7 must be final before the first EU ingest.

Scope

Which services get an EU instance

decided
Decided: the EU stack only gets EU instances of services that touch customer content. Everything else is shared with the US account (configuration only) or disabled on EU.

EU accounts and resources (cards 1–8): Vercel project in fra1, Supabase eu-central-1, Upstash eu-central-1, Cloudflare R2 EU-jurisdiction buckets, Azure AI Services in swedencentral (LLMs, embeddings and Cohere rerank), Trigger.dev project with eu-central-1 workers, a Modal eu environment for the parsing service, and EU processing on every Datalab request (existing Datalab account and key, processing_location=eu).

Shared with the US, configuration only (cards 9–14): Cloudflare DNS records, Stripe (EU webhook endpoint and restricted key), Resend (same account, key and sender domain), GitHub and Google OAuth (EU callback added to the existing app and client), Discord (optional).

Disabled on EU: managed vector stores, PostHog, Tinybird webhook delivery logs, the ZeroEntropy reranker, crawl and YouTube ingestion, demo templates, and browser third parties (model logos, generated avatars, the CDN-hosted image worker, the Cal.com embed). See the list below the cards.

Vectors

Vector store on EU

decided
Decided: bring your own key, required. No managed vector store on EU: no Agentset Turbopuffer organization and no managed Pinecone. EU namespaces use the customer's own Turbopuffer or Pinecone credentials.

Turbopuffer namespaces on EU must use an EU region (aws-eu-central-1, aws-eu-west-1, gcp-europe-west3 or gcp-europe-west1); other regions are rejected with a clear error. Pinecone: the customer controls the index region, so the dashboard shows a notice recommending an EU index. The code change is E10, a launch blocker.

D1

Hostnames

decided
Decided: Option A (final). Dashboard at eu.agentset.ai, API at api.eu.agentset.ai, customer hosting CNAME target cname.eu.agentset.ai. Needs the env-driven hostname refactor (R3).

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 (not chosen) needed no code: NEXT_PUBLIC_APP_SHORT_DOMAIN=eu.agentset.ai for app.eu.agentset.ai + api.eu.agentset.ai, with eu.agentset.ai redirecting to app.eu, at the cost of a longer dashboard URL.

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

D2

Residency tier

decided
Decided: 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

decided
Decided: 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

decided
Decided: 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–8 set up the EU resources that hold or process customer content (card 8 only configures EU processing on the existing Datalab account). Cards 9–14 only change configuration in accounts shared with the US; no new accounts. All of them only provision and collect values; nothing is deployed until card 15, which runs the first EU deploy in a fixed order. Work through them in order: later services need values from earlier ones. Hostnames follow D1 (Option A). Placeholders in angle brackets come from the vendor's console. Each card's status line shows what was done on 24 Sep 2026; the checkboxes are yours to tick.

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

EU accounts & resources

Services that store or process customer content. Each gets its own EU instance; Datalab instead processes every EU request in the EU on the existing account.

1

Vercel — EU project & domains (no deploy yet)

functions fra1Pro plansame teamcontrol plane & logs US

Done 24 Sep 2026: project created (functions fra1, build settings mirrored from the US project, system env vars exposed, not Git-connected, not deployed); the three EU hostnames are attached and verified.

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 15.
  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 15 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

Supabase — EU Postgres

eu-central-1 Frankfurtexisting orgcompute: mirror US

Partly done 24 Sep 2026: project created in eu-central-1 with SSL enforcement on, and a TLS 1.3 connection verified. Remaining: turn off the Data API (dashboard).

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.
    DATABASE_URL=postgresql://…:6543/postgres?pgbouncer=true
    DIRECT_URL=postgresql://…:5432/postgres?sslmode=require
    Leave sslmode out of DATABASE_URL: with workstream L1 (Postgres client hardening) the app connects to Supabase hosts over TLS and verifies the certificate against the pinned Supabase root CA, and an sslmode in the URL would override that. DIRECT_URL is only used by the Prisma CLI for migrations and keeps its TLS parameters.
  6. Order: L1 merged → test (card 15's migration status check and a sign-in) → then turn on Database → Settings → SSL Configuration → Enforce SSL. No CA download is needed: L1 ships the CA in code.
  7. The schema is applied in card 15 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.
3

Upstash — EU Redis

primary eu-central-1no read regionseviction off

Not started: waiting on the Upstash CLI login.

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.
4

Cloudflare R2 — EU buckets

jurisdiction eu3 buckets.eu. endpoint

Partly done 24 Sep 2026: the three EU-jurisdiction buckets exist, with CORS on the uploads bucket mirroring the US bucket. Cloudflare placed them in its Eastern Europe (EEUR) location, which is still in the EU. Remaining: the R2 API token (dashboard only) and the custom domains.

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
5

Azure AI Services — LLMs, embeddings & Cohere rerank

swedencentralkind AIServices · S0DataZoneStandard onlyno Cohere account

Done 24 Sep 2026: resource and all seven DataZoneStandard deployments (including both Cohere rerankers), auto-upgrade off, all smoke-tested: responses, 3072-dimension embeddings, and rerank on /providers/cohere/v2/rerank with Bearer or api-key auth. Remaining: assign the deny-Global policy (step 3).

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).

Cohere rerank on the same resource (Azure Foundry)

  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).
6

Trigger.dev — EU project

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

Partly done 24 Sep 2026: EU project created. Remaining in the dashboard: set the default region to eu-central-1 and create the production API key.

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 15, once every value exists. Don't add HTTP, Prisma or OpenAI OpenTelemetry instrumentations to the EU config.
7

Modal — EU parsing service

region eurouting eu-west (Dublin)separate environment

Partly done 24 Sep 2026: environment eu created. The secret and the deploy wait for P1–P6 and the Upstash and R2 values.

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). The Datalab key is the existing one, shared with the US stack. 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=<existing key, shared with US> \
      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.
8

Datalab — EU processing on the existing account

shared account · config onlyprocessing_location=eu · NLaccount plane US

Remaining: written confirmation that the DPA covers EU processing. The shared API key is copied from the US configuration when the stack is wired.

How to set it up

  1. Nothing to create: the EU cell uses the existing Datalab account and API key, shared with the US stack. Each EU request asks for EU storage and EU processing with processing_location=eu.
  2. Ask Datalab to confirm in writing that the existing DPA covers EU-processing requests on this account.
  3. In account settings, make sure the data-retention consent (model-training opt-in) is off.
  4. The parsing service (workstream P4) uses Datalab's File Upload API on EU. The file is streamed from the EU bucket inside the EU parsing service, so Datalab never receives a link to the bucket:
    POST /api/v1/files/upload        {filename, content_type, "processing_location":"eu"}
                                     → file_id, upload_url (EU storage, 1 h), datalab://file-…
    PUT  <upload_url>                 the file bytes
    GET  /api/v1/files/{file_id}/confirm
    POST /api/v1/convert             file_url=datalab://file-…, processing_location=eu,
                                     output_format / mode as today → poll request_check_url
    DELETE /api/v1/files/{file_id}   and DELETE /api/v1/results/{request_id} after reading
    All calls go to https://www.datalab.to with the X-API-Key header. Maximum file size 200 MB.
  5. Test once with a sample PDF through the same flow.
  6. 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); the EU sub-processor list.
  7. Never use Datalab's public Playground with customer documents.

Shared with the US: configuration only

No new accounts. These services don't store customer content, so the EU stack reuses the US account and only adds EU settings.

9

Cloudflare DNS — EU hostnames

shared · records onlyagentset.ai zoneDNS only (grey cloud)no content

Partly done 24 Sep 2026: the Vercel domain-verification TXT records for the three EU hostnames are in place.

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. If the zone has CAA records, make sure 0 issue "letsencrypt.org" is allowed, or Vercel can't issue certificates.
  6. Wait for "Valid Configuration" on each Vercel domain card. A _vercel TXT record is only needed if Vercel says another team uses the domain.
  7. The R2 custom domains from card 4 are created by R2 itself. No mail records are needed: EU email reuses the US sender domain (card 11).
10

Stripe — EU endpoint & key

shared account · config onlyUS 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 15 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.
11

Resend — reuse the US account

shared account · config onlysame sender domainmetadata stored US

How to set it up

  1. Nothing to create in Resend: the EU stack reuses the existing account, API key and sender domain. No EU sending domain and no new DNS records.
  2. Copy the US RESEND_API_KEY into the EU Vercel project and the EU Trigger.dev project (Trigger values are set in card 15).
  3. Set APP_DOMAIN=https://eu.agentset.ai on both. After workstream C1, every email link (sign-in, invites, billing and webhook notices) is built from it, so EU emails point to eu.agentset.ai. Leave EMAIL_FROM_DOMAIN unset.
  4. List Resend in the EU sub-processor list as a US processor of email metadata (the existing DPA covers the account).
12

GitHub — add the EU callback to the existing OAuth app

shared app · config onlyexact-match callback

How to set it up

  1. github.com/organizations/agentset-ai/settings/applications (Org → Settings → Developer settings → OAuth Apps) → the existing production OAuth app.
  2. Under callback URLs, add https://eu.agentset.ai/api/auth/callback/github next to the existing US callback (OAuth apps accept up to 10 callback URLs since August 2026). Make sure wildcard matching and Device Flow are off. Save.
  3. Copy the app's existing Client ID and client secret into the EU Vercel project. No new secret is needed. Sign-in is tested in card 15.
  4. Optional: a separate "Agentset EU" app (Org → Developer settings → OAuth Apps → New OAuth App, same callback) gives the EU its own secret and consent-screen name.
13

Google — add the EU redirect URI to the existing web client

shared client · config onlyno re-verification

How to set it up

  1. console.cloud.google.com → the project that owns the current production client → Google Auth Platform → Clients → the existing production Web client.
  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. Authorized redirect URIs → Add URIhttps://eu.agentset.ai/api/auth/callback/google → Save. Keep the US redirect URI.
  4. Copy the client's existing Client ID and secret into the EU Vercel project. Sign-in is tested in card 15 (new redirect URIs can take a few minutes to work).
14

Discord — optional alerts

shared · optionalIDs onlyUS service

How to set it up

  1. Pick one: reuse the existing US webhook URLs (workstream A3 prefixes every EU message with [EU]), or leave the variables unset and alerts fall back to the function logs.
  2. Policy either way: alerts carry IDs, slugs, plan names, event types and counts only. No emails, names, document names, queries or raw customer error payloads.
  3. Separate EU channels are optional later: in the internal server, create the channels, then Edit Channel → Integrations → Webhooks → New Webhook → Copy Webhook URL. Treat the URLs as secrets.

Deploy

15

First EU deploy

after cards 1–14fixed 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 E4 the reranker would call Cohere's own API instead of the Azure EU deployment. The R4 guards refuse to start when E4 settings are missing or when a variable of a service that is disabled on EU is set.
  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 7) 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 10).
  11. Smoke tests: GitHub and Google sign-in on the EU host; create a namespace with a test Turbopuffer key in an EU region, and check that a managed vector store and a non-EU Turbopuffer region are rejected; 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 2) and continue with Phase 3.

Disabled on EU, not provisioned

  • Managed vector stores: no Turbopuffer EU organization and no managed Pinecone. EU namespaces use the customer's own Turbopuffer (EU regions only) or Pinecone credentials, and the managed options are rejected (E10). Demo templates are disabled too, because they seed a managed vector store.
  • PostHog product analytics: no key is set, and the EU build loads neither the PostHog script nor its proxy rewrites (A1).
  • Tinybird webhook delivery logs: nothing is logged on EU, and the dashboard's delivery-log view says "not available in this region" (J3).
  • ZeroEntropy reranker: zerank models are hidden and rejected on EU (E5). Cohere rerank on Azure is the EU reranker.
  • 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.
  • Browser third parties: model logos, generated avatars and the image-compression worker are served by the app itself, and the Cal.com embed becomes a plain link (A5).
  • 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 follow D1 (Option A). Variables of services that are disabled on EU are grouped at the end as "not set on EU", each tied to the code item that makes it optional.

required must be set (validation or guard), value unused here optional, may be unset not usednewoptional on EUnot set on EUshared with US
VariableEU value / sourceVercel EUTrigger EUModal EUNotes
Region & hostnames
Values follow D1 = Option A (decided): dashboard eu.agentset.ai, API api.eu.agentset.ai, custom-domain target cname.eu.agentset.ai.
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
<the existing OAuth app's values>shared with US Same OAuth app with the EU callback added (card 12). Only differs if a separate EU app is created.
GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET
<the existing web client's values>shared with US Same web client with the EU redirect URI added (card 13).
Database & Redis
DATABASE_URL<Supabase transaction pooler :6543>?pgbouncer=trueMust differ. No sslmode: L1 verifies TLS against the pinned Supabase root CA, and a URL sslmode would override it. Test before turning on Enforce SSL. Also needed at build time (Prisma generate).
DIRECT_URL<Supabase session pooler :5432>?sslmode=requireMigrations only: .env.eu.local or a GitHub secret for the migrate workflow. Same TLS parameters; check with db:status:eu.
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.
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>EU-specific. Separate restricted key on the shared account, so it can be revoked on its own.
NEXT_PUBLIC_STRIPE_PUBLIC_KEY<account pk_live_ key>shared with US Jobs: required by the Stripe env import until J6.
STRIPE_WEBHOOK_SECRET<whsec_ of the EU endpoint>EU-specific (EU endpoint). Must differ.
Email
RESEND_API_KEY<the existing US key>shared with US Same account and sender domain. Jobs send the webhook-failure emails.
Alerts
DISCORD_HOOK_ALERTS
DISCORD_HOOK_CRON
DISCORD_HOOK_SUBSCRIBERS
DISCORD_HOOK_ERRORS
<the US webhook URLs> or unsetshared with US Optional. EU messages carry an [EU] tag and IDs only (A3). Unset, alerts fall back to the function logs.
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
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<the existing Datalab key>shared with US Same account; EU processing is requested per call.
DATALAB_PROCESSING_LOCATION neweuSent on every Datalab upload and convert request (P4). 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.
Not set on EU (disabled services)
Leave these unset on Vercel EU and Trigger EU. A copied US value would point the EU cell at a US account, so the R4 guard refuses to start if any of them is set. The ones the engine's env validation requires today become optional through the named code item, which must land before the first EU deploy.
DEFAULT_TURBOPUFFER_API_KEYunset (no managed vector store)not set on EU Required by the engine env today; optional after E10. EU namespaces use the customer's own key.
DEFAULT_TURBOPUFFER_REGION newunsetnot set on EU Introduced by E1 for the US managed store (defaults to aws-us-east-1). EU namespaces carry their own region.
DEFAULT_PINECONE_API_KEY
DEFAULT_PINECONE_HOST
unsetnot set on EU Required by the engine env today; optional after E10.
SECONDARY_PINECONE_API_KEY
SECONDARY_PINECONE_HOST
unsetnot set on EU Required by the engine env today; optional after E10.
DEFAULT_ZEROENTROPY_API_KEYunset (ZeroEntropy off)not set on EU Required by the engine env today; optional after E5, which also hides the zerank models on EU.
TINYBIRD_API_URL
TINYBIRD_API_KEY
unset (no delivery logs)not set on EU J3 makes them explicitly optional and skips delivery logging when they are unset.
NEXT_PUBLIC_POSTHOG_KEYunset (no analytics)not set on EU Already optional; A1 also keeps the PostHog script and its proxy rewrites out of the EU build.
EMAIL_FROM_DOMAINunsetnot set on EU Not needed: EU reuses the US sender domain. Only relevant if a separate EU sender domain is added later.
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

59 items · 35 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, Cohere base URL, S3 endpoint, Redis and parsing URL are on an EU allowlist; that the variables of services disabled on EU (managed vector store, ZeroEntropy, Tinybird, PostHog) are unset; 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

Shared change for the US managed store; not used on EU, where the vector store is bring-your-own. 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). E10 needs gcp-europe-west1 in the enum: land that part with whichever item ships first.

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

Sno
E2Managed vector store default (US)

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. The EU side (no managed stores, optional env) moved to E10.

packages/validation/src/vector-store/index.ts · apps/web/src/components/create-namespace/vector-store-step.tsx

Sno
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 off on EU

Hide and reject the zerank-* rerankers on EU (UI, hosting config, API) with a clear 4xx; make DEFAULT_ZEROENTROPY_API_KEY optional in the engine env so the EU build doesn't need it. US unchanged.

packages/engine/src/env.ts · packages/engine/src/rerank/zeroentropy.ts · packages/engine/src/rerank/index.ts · packages/validation/src/re-ranker/constants.ts · packages/validation/src/re-ranker/index.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

Optional hardening for managed vector stores. Not needed on EU: EU namespaces live in the customer's own vendor account.

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

Sno
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. On EU the URL only reaches the parsing service: P4's upload flow means Datalab no longer receives it.

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

Sno
E10Bring-your-own vector store on EU (decided 24 Sep 2026)

On EU, namespace create (API and dashboard) rejects MANAGED_TURBOPUFFER, MANAGED_PINECONE and MANAGED_PINECONE_OLD with a clear 4xx, and a vector store config becomes required (no managed default); the dashboard hides the "Managed" option. DEFAULT_TURBOPUFFER_API_KEY and DEFAULT_/SECONDARY_PINECONE_* become optional in the engine env, with a typed error if a managed store is used without them. On EU, BYO Turbopuffer region must be aws-eu-central-1, aws-eu-west-1, gcp-europe-west3 or gcp-europe-west1, and other regions get an error naming the allowed ones. BYO Pinecone shows a notice that the customer controls the index region and that an EU index is recommended. Demo templates are disabled on EU, because seeding them needs a managed store. US unchanged.

packages/engine/src/env.ts · packages/engine/src/vector-store/index.ts · packages/validation/src/vector-store/index.ts · packages/validation/src/vector-store/turbopuffer.ts · apps/web/src/schemas/api/namespace.ts · apps/web/src/services/namespaces/create.ts · apps/web/src/services/namespaces/validate.ts · apps/web/src/components/create-namespace/vector-store-step.tsx · apps/web/src/server/api/routers/namespaces.ts · apps/web/src/app/app.agentset.ai/(dashboard)/[slug]/namespaces-empty-state.tsx · packages/jobs/src/tasks/seed-demo-namespace.ts

Mblocker

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
J3Webhook delivery logs off on EU

TINYBIRD_API_URL / TINYBIRD_API_KEY become explicitly optional (validated with createEnv). When they are unset, the webhook task skips delivery logging and the dashboard's delivery-log view says "not available in this region". A logging failure after a successful delivery never makes the task retry and redeliver.

packages/tinybird/src/client.ts · packages/jobs/src/tasks/send-webhook.ts · apps/web/src/server/api/routers/webhooks.ts · apps/web/src/app/app.agentset.ai/(dashboard)/[slug]/webhooks/[webhookId]/page.tsx

Sblocker
J4Retention and redaction for webhook delivery logs

US only (no delivery logs on EU). 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

Only if demo templates are ever enabled on EU (they are disabled by E10, since they need a managed vector store): 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 via file upload

When DATALAB_PROCESSING_LOCATION=eu, stream the file from the EU bucket to Datalab's EU storage with the File Upload API (upload request with processing_location=eu → PUT to the presigned upload URL → confirm), then call /api/v1/convert with the returned datalab:// reference and processing_location=eu; poll request_check_url; download result_url (EU results aren't returned inline); delete the uploaded file and the result after reading. Datalab then never receives a presigned bucket URL. Move from the legacy /api/v1/marker to /api/v1/convert. Files up to 200 MB.

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

For Supabase hosts, connect over TLS and verify the server certificate (hostname included) against the pinned Supabase root CA, in both the app and the jobs client; other hosts (local Postgres) are unchanged, and an explicit sslmode in the URL still takes precedence. 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/src/ssl.ts (new) · 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
A1No PostHog on EU

When the region is EU or no key is set, don't load posthog-js at all (lazy import behind the check) and don't add the /_proxy/posthog rewrites; the server-side client is a no-op. US keeps its current hosts.

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

Sblocker
A2Analytics data minimisation

US hardening; PostHog is off on EU. 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

Sno
A3IDs-only alerts with a region prefix

Alerts carry IDs, slugs and counts only; region prefix ([EU] / [US]), so EU can share the US webhooks.

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 from the region's app 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. EU reuses the US sender domain, so from-addresses don't change and no EMAIL_FROM_DOMAIN is needed.

packages/emails/src/env.ts · send.ts · templates/*.tsx · 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

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, the bring-your-own vector store requirement and allowed regions on EU, features disabled on EU, 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

  • D1–D4 decided (24 Sep 2026): Option A hostnames, Tier 1, unadvertised self-serve, BYO US embeddings blocked; plus the EU-instances-only-for-customer-content scope and the bring-your-own vector store. First provisioning action: create the EU Vercel project and add its domains (card 1).
  • D5–D7 start on their defaults; D7 must be final before the first EU ingest.
  • Start the long-lead items: Datalab's written confirmation that the existing DPA covers EU processing; Azure Data Zone quota requests (LLMs and Cohere rerank); 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 the EU accounts that need nothing from code: Supabase project, Upstash database, R2 buckets, Azure resource and deployments (including Cohere rerank), Trigger.dev project (most of these were done on 24 Sep 2026; see the provisioning status). With D1 final, also: the EU callbacks on the existing GitHub app and Google client, the Stripe endpoint (then disable it) and the R2 CORS rule.
1

Code foundation no EU accounts needed

  • Region foundation R1–R5 and engine E3–E6 and E10 (bring-your-own vector store). Env-gated changes default to today's US behaviour and merge safely; E3's response-storage setting changes 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 and A3–A5, emails C1, snippets C2, OpenAPI C3 (to US production).
  • Lock before the first EU namespace, ingest or API key: E10 (bring-your-own vector store and EU region check), L2 key prefix and D7 image URLs. 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–14 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 15, 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
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× per research ($5 per 1K pages balanced, $12.50 accurate)No separate plan: existing account, shared with US
Cloudflare R2No jurisdiction surchargeDLS is an Enterprise add-on (Tier 2 only)
Resend · GitHub · Google · DiscordShared with US, no EU differenceNo extra cost
StripeEU cards on a US account pay international and FX surchargesExtra endpoints and keys are free
Vector storeBring your own: billed to the customer's Turbopuffer or Pinecone accountNo Agentset cost on EU
Turbopuffer · ZeroEntropy · Tinybird · PostHogNot used on EUNone
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: that the existing DPA covers EU-processing requests; API front-door location; LLM sub-processors for EU requests (captions, chart understanding, accurate mode).
  • Azure: Cohere rerank Data Zone price and default quota.
  • 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?