An EU-sovereignty checklist for a Laravel SaaS

One page to hand to a client or DPO, with every part of a Laravel SaaS that holds or sees data, what to check for each and how to note the exceptions.

In this post
  1. What does an EU-sovereignty checklist need to show?
  2. How to fill it in
  3. The checklist
  4. Notes per row
  5. An example, filled in
  6. What to hand over
  7. Summary

Sooner or later a client, a tender or a data protection officer asks for it in writing: which parties see our data, where, and under which law? A sentence like "hosted in the EU" does not answer that. A table does.

This post is that table, for a typical Laravel SaaS. Copy it, fill it in for your app, and hand it over. It builds on How to host a Laravel app EU-sovereign, which explains the setup; this one is about writing it down.

What does an EU-sovereignty checklist need to show?

For every part of your app that holds or sees data, it needs three facts: where the data is stored, which company runs that part, and in which country that company is headquartered. Add the exceptions you know about. That is enough for a DPO to judge the setup, and it is honest about the parts you cannot control.

The reason ownership matters, and not just location, is the US CLOUD Act: a US provider can be required to hand over data it controls, wherever that data is stored. The CLOUD Act explained covers that in detail.

How to fill it in

Start from the code, not from memory:

# Every outside service your app talks to
grep -rhoE "https?://[a-z0-9.-]+" config/ app/ | sort -u
grep -E "^[A-Z_]+_(KEY|SECRET|TOKEN|DSN|URL)=" .env.example
composer show --direct

Every SDK in composer.json, every host in config/services.php and every DSN in .env.example is a candidate row. Then add what does not show up in code: where builds run, where secrets are stored, and the tools your team uses.

The checklist

# Part What it sees Where is it stored? Who runs it? Company HQ Contract with
1 Runtime (web, worker, scheduler) Every request and response
2 Database Everything
3 File storage Uploads, exports, invoices
4 Queue Job payloads
5 Cache and sessions Session data, cached queries
6 Secrets APP_KEY, passwords, API keys
7 Builds Source code, build-time variables
8 CDN, DNS, firewall Every visitor's request and IP address
9 Deploy tool Code, secrets, keys to your cloud
10 Transactional mail Names, addresses, message content
11 Payments Names, payment details
12 Error tracking Stack traces, request data, user IDs
13 Logs Whatever the app logs
14 Analytics Visitor behaviour, IP addresses
15 Fonts, scripts from CDNs Every visitor's IP address
16 Support tools (chat, helpdesk) Customer conversations
17 Team tools (source hosting, CI, chat, docs) Code, credentials, sometimes data

Leave nothing blank. "Not used" is a valid answer; an empty cell is not.

Notes per row

1–5. Runtime, database, files, queue, cache. Keep them at one provider, in one region, so data does not cross between companies on its way from the web container to the database. On serverless, sessions and cache belong in the database or a cache service, since the container's disk does not outlive it.

6. Secrets. Ask where the values are stored and who can read them. A deploy tool that keeps a copy of your .env is a party that holds your database password.

7. Builds. A build sees your full source code and often build-time secrets. Hosted CI and hosted build servers count as a party here, with their own owner and location.

8. CDN, DNS, firewall. The edge sees every request before your app does, including IP addresses and often cookies. Check where the edge locations are, whether they can be limited to Europe, and where DNS is answered.

9. Deploy tool. It holds keys to your cloud. Check whether it sits in the request path, whether it stores secret values and logs, and what happens to your app if you stop using it.

10–14. Mail, payments, errors, logs, analytics. These often carry the most personal data per request. Error trackers in particular receive request bodies, headers and user IDs by default. See European alternatives for the services a Laravel app calls.

15. Fonts and scripts. Each external font or script sends the visitor's IP address to whoever serves it. Self-host them through Vite and the row is answered.

16–17. Support and team tools. These are about your organisation, not the app's runtime. List them separately so the reader can see the difference, but list them.

Warning

Do not mark a row as sovereign because the data centre is in the EU. The "Company HQ" column is the one that answers the CLOUD Act question.

An example, filled in

This is the hosting part for a Laravel app deployed with Brewless, as described in the Brewless documentation:

Part Where is it stored? Who runs it? Company HQ Contract with
Runtime Scaleway region you choose (Paris, Amsterdam, Warsaw or Milan) Scaleway Serverless Containers France You
Database Same region Scaleway France You
File storage Same region Scaleway Object Storage France You
Queue Same region Scaleway Queues France You
Secrets Your own Scaleway Secret Manager Scaleway France You
Builds Inside your own Scaleway project Scaleway France You
CDN, DNS, firewall bunny.net; edge locations can be limited to Europe bunny.net Slovenia You
Deploy tool Control plane at Scaleway in Paris Brewless (Coding Agency) The Netherlands You

And the notes that belong with it:

  • Brewless is not in the path of visitor traffic: requests go from bunny.net straight to your containers.
  • Brewless keeps the names and revisions of your environment variables, not their values. Logs are read live from Scaleway and not stored by Brewless.
  • bunny.net answers DNS lookups from its worldwide network and uses a few US-based tools for its own support and email, which see account details, not your visitors' traffic.
  • Queues are not available in Warsaw or Milan yet, and file storage is not yet in Milan, so check the regions before you pick one.

Rows 10 to 17 depend on your app and your team, and Brewless does not answer them.

Tip

Put the filled-in checklist in your repository, next to brewless.yml. A pull request that adds a new SDK is then the natural moment to add a row.

What to hand over

A one-page document with:

  1. The filled-in table.
  2. The exceptions, each in one sentence: what data, which provider, which country, and why you accept it.
  3. The data processing agreements you hold, and with whom. GDPR and your hosting stack explains which ones those are.
  4. The date, and who is responsible for keeping it current.

A DPO can work with a short list of known exceptions. What they cannot work with is a claim of full sovereignty that turns out not to hold the first time someone looks at config/services.php.

Summary

  • List every part that holds or sees data, from runtime to team tools.
  • For each, write down location, operator, company headquarters and who holds the contract.
  • Build the list from the code: SDKs, hosts and DSNs.
  • Write the exceptions down instead of leaving them out.
  • Keep it in the repository and update it with every new outside service.

Frequently asked questions

What should an EU-sovereignty checklist for a Laravel app cover?

Every part that holds or sees data - the runtime, database, files, queue, secrets, builds, CDN and DNS, the deploy tool, and every outside service the app calls, such as mail, payments, error tracking and analytics. It should also cover the team's own tools, because they see code, data and credentials.

Who should fill in the checklist?

The developers who know the stack fill in the facts, and the client or their data protection officer judges them. Developers know which services the app calls; the DPO decides which exceptions are acceptable.

Is an EU region of an American cloud enough to tick the runtime box?

No. The data is in the EU, but the provider is subject to US law, including the CLOUD Act. The checklist asks for both the location and the owner of each provider.

How often should the checklist be updated?

Whenever a dependency that talks to the outside world is added or replaced, and at least once a year. A new SDK in composer.json can add a new party that sees your data.

Does Brewless fill in parts of the checklist?

It answers the runtime, database, files, queue, secrets, builds and CDN rows with Scaleway (France) and bunny.net (Slovenia) in your own accounts. Brewless itself is a Dutch control plane that is not in the visitor traffic path and does not store secret values. The services your app calls remain your choice.

All posts Read this post as markdown

Try it on your own app

Brewless deploys your Laravel app to Scaleway and bunny.net, in your own accounts. We open access to Laravel teams in small batches.