EU Data Residency for Submissions and File Uploads
Every form backend makes a quiet decision on your behalf: where the data lands. For most of them the answer is "somewhere in the United States", and you only find out when a customer sends you a DPA questionnaire asking which country their applicants' CVs are stored in.
Static Forms now lets you make that decision yourself. Pick European Union when you sign up and your form data — submissions, file attachments, webhook delivery logs and AI reply logs — is stored in AWS eu-central-1 (Frankfurt) instead of us-east-1. Not replicated to the US, not cached in the US, not written to a US table first and copied over. The row and the uploaded file are created in Frankfurt and read back from Frankfurt.
This post is the engineering detail behind that sentence: how a submission is routed, where an attachment physically sits, how the download link is signed, and — the part most residency announcements skip — the things that deliberately stay in the United States for every account. The exact legal scope is in our DPA; this is the same scope, written for developers.
The split: control plane vs data plane
The architecture has one rule, and everything else follows from it. Account state is global. Form data is regional.
| Stays global (US) | Regional (follows your pick) |
|---|---|
| Your user record, login, billing | Form submissions |
| Form/workspace configuration and API keys | File attachments (S3 objects + file records) |
| Rate-limit counters, storage quota counters | Webhook delivery logs |
| Email delivery (SES) | AI reply logs |
| Operational request logs | Submission retention and cleanup |
Why keep the control plane global? Because it is what lets there be one product. There is no eu.staticforms.dev, no second dashboard, no separate login, no parallel release train, and no second API key to manage. You log in at staticforms.dev, and your API key resolves the same way regardless of region. Only the tables and buckets your submissions live in change.
Your account record — owner email and anonymized consent IP — stays in the US
userstable for every account, including EU ones. We state that plainly in the DPA rather than engineering around it, because pretending otherwise would be the interesting kind of lie.
What happens to a submission
There is a single ingest endpoint for everyone: https://api.staticforms.dev/submit. You do not swap it for a regional URL, and your HTML does not change.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Contact Us</title>
</head>
<body>
<!-- Identical markup for a US-region and an EU-region account -->
<form action="https://api.staticforms.dev/submit" method="POST">
<input type="hidden" name="apiKey" value="YOUR_API_KEY" />
<label for="name">Name</label>
<input type="text" id="name" name="name" required />
<label for="email">Email</label>
<input type="email" id="email" name="email" required />
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
<input type="hidden" name="redirectTo" value="https://yoursite.com/thank-you" />
<button type="submit">Send</button>
</form>
</body>
</html>Behind that request:
- The API key is resolved in the global control plane. One lookup tells us which account owns the form, what plan it is on, and which region it is pinned to.
- Validation runs before anything is stored — spam checks, CAPTCHA, rate limits, plan limits. Nothing region-specific here.
- The submission row is written to the owning account's region. For an EU account that is the Frankfurt
form_submissionstable. Not a US table with a region flag on it — a different table, in a different AWS region. - Every later write follows the same route. Delivery status, read/unread, spam and not-spam, timeline entries, webhook attempt logs, AI reply logs. They are all keyed to the region resolved in step 1, including the ones written asynchronously by queue consumers, because the region travels inside the queue payload.
The consequence worth internalizing: an EU account's submission never exists as a row in us-east-1. We verified that the hard way before shipping — after a full end-to-end pass (signup, submission, attachment, inbox, mutations, export, webhook, AI reply, deletion) the US tables held zero rows for that account across submissions, file uploads, webhook logs and AI reply logs.
What happens to a file attachment
File uploads are the part where residency usually leaks, because a file has two lives: the bytes in object storage, and the metadata row that makes them findable. Both are regional.
Adding uploads to the form above is one attribute and one input — again, identical for both regions:
<form
action="https://api.staticforms.dev/submit"
method="POST"
enctype="multipart/form-data"
>
<input type="hidden" name="apiKey" value="YOUR_API_KEY" />
<input type="text" name="name" required />
<input type="email" name="email" required />
<label for="document">Attach your CV (PDF, max 4.5MB)</label>
<input type="file" id="document" name="document" accept=".pdf,.doc,.docx" />
<button type="submit">Send</button>
</form>What the platform does with document:
- ✅ The bytes are uploaded to your region's private bucket. Each region has its own S3 bucket — bucket names are globally unique, so this is a genuinely separate bucket in a separate AWS region, with Block Public Access on and
AES256server-side encryption. - ✅ The file record is written to your region's
file_uploadstable, alongside the submission it belongs to. The record — original filename, MIME type, size, object key — is data-subject content, so it lives where the submission lives. - ✅ Downloads are signed against the regional bucket. Clicking a download in your Inbox mints a fresh presigned
GETURL that expires in 5 minutes, signed for the bucket the object is actually in. For an EU account the bytes travel from Frankfurt to your browser — there is no US proxy hop and no US copy to serve from. - ✅ Storage quota accounting stays global. The counter is a number of bytes on your account record, not form data, so it lives in the control plane with the rest of your account state. The bytes it counts never leave your region.
- ✅ Expiry and deletion happen in region. Delete a submission and its file records and S3 objects are removed from your region's table and bucket. The retention sweep visits every region rather than assuming US, so an EU account's expired attachments are actually collected instead of lingering forever.
One deliberate failure mode, since it affects what you see: if persistent storage is unavailable for a file, the submission is still accepted and the file is still delivered by email, but no download button appears for it in the inbox. A storage problem never costs you a lead — and it never quietly falls back to storing the file in another region either.
File uploads are a paid-plan feature (4.5MB per file). See file uploads in the docs for limits and framework snippets, or the walkthrough in Next.js contact form with document upload.
Teams that span regions
An agency owner pinned to the EU can invite a teammate whose own account is US-region — or who signed up before regions existed at all. Routing that teammate's reads by their region would query an empty Frankfurt-less table and render the workspace as if it had no data.
So routing follows the account that owns the data, never the person looking at it. A US-based teammate opening an EU workspace reads from Frankfurt, and when they mark a submission read, reply, or unmark spam, the write lands in Frankfurt too. Nothing about cross-region team access pulls a copy into the US.
What EU residency does not move
This is the section we would rather you read twice than discover later. Selecting the EU region relocates storage of form data. It does not relocate everything, and we will not market it as "your data stays in the EU".
- Email delivery is US, for every account. Notification and auto-responder emails are sent through AWS SES in
us-east-1, with Singapore (ap-southeast-1) as delivery failover. The email carries the submission, so this is a transmission out of the region — disclosed in our DPA §8 and in the Transfer Impact Assessment, and covered by SCCs / the EU-U.S. Data Privacy Framework. SES does not retain message content, but it does keep a per-region suppression list of recipient addresses. - Operational request logs are US, for every account. Request metadata — IP, user agent, referer, delivery status, and form field names — is logged in the US for 365 days. Field values are never logged, by design and enforced in code: the submit path logs the keys of your payload, never the contents. Keeping this pipeline single-region is what keeps our abuse tooling working; an attacker should not be able to buy log immunity by picking a region at signup.
- Account and billing data are US, for every account (see the table above).
The supportable claim is precise: form data stored in the EU, email delivered from the US, operational logs in the US. That is what the DPA, the sub-processor table, Annex III and the TIA all say, and it is what the product actually does.
Choosing and changing your region
- At signup. The region picker sits above both signup paths — email and Google — and defaults to United States. It applies to the account on its first write, so an OAuth signup gets the region you picked, not a US default it has to be rescued from later.
- After signup, while you are empty. You can change the region from your account page for as long as the account holds no stored form data. The check runs server-side across every region — submissions and file uploads, not just the pinned region — and fails closed: if the check itself errors, the change is refused. Refusing a change is recoverable; splitting an inbox across two continents is not.
- Once your first submission arrives, the pin is final. Not a policy choice, a physical one: the rows and objects are in that region, and there is no migration that keeps a live inbox coherent while it runs. The account page shows the pin as locked with the reason.
Pick deliberately, then. If you are collecting personal data from EU residents — job applications, patient intake, member records — the EU region is the one that removes the third-country transfer for that data at rest.
Adding regions from here
The region registry is config, not code: a region is an entry with an AWS region, a label, an uploads-bucket environment variable and an email-send region. Every submission, file, log and cleanup path resolves through that registry, so adding a third region (APAC is the obvious next one) means provisioning infrastructure and adding a registry entry — not auditing hundreds of call sites again.
If you need residency in a geography we do not serve yet, tell us which one. That request is now a provisioning ticket rather than a rewrite.
Get started
- New account? Choose your region on the signup page — United States (default) or European Union (Frankfurt).
- Existing account with no submissions yet? Set it from your account page before your first submission lands.
- Need the exact wording for a customer questionnaire? The DPA, the sub-processor list and the Transfer Impact Assessment all name the regions, the sub-processors and the transfer mechanisms line by line.
- Just getting started with forms? Begin with Getting started with Static Forms, then add privacy-first spam protection with Altcha — no third-party tracking, which pairs well with an EU-resident inbox.
That's it. Same endpoint, same API key, same dashboard — your form data just lives where you need it to.
Related Articles
WordPress Form File Upload: Ultimate Guide 2026
Learn WordPress form file upload with WPForms, custom PHP, and API solutions for headless sites. Includes code & security best practices.
hCaptcha Best Practices
A practical guide to using hCaptcha, including setup decisions, common mistakes, and implementation planning tips.
How to Build an Application Form with HTML & JavaScript (2026)
Create a professional application form with file uploads for job applications and program registrations. Complete HTML, React, and Next.js examples with validation and spam protection.