Designing a CRM that respects EU data residency
Data residency is not a checkbox you tick at the end. It is an architecture decision that touches where you host, how tenants are isolated, and how many layers stand between a request and someone else's data.
For a European B2B tool, "where does the data live" is one of the first questions a serious buyer asks, and it deserves a real answer rather than a badge on a pricing page. Data residency is not a compliance checkbox you bolt on at the end. It is an architecture decision, and if you make it late you usually cannot make it well.
Start with the obvious layer: hosting. Propel runs on Supabase Postgres, and the data lives in the EU. That is the baseline, and it is necessary, but it is nowhere near sufficient. Hosting data in the right region tells you nothing about whether one customer can reach another customer's records. Residency answers "where"; the harder question is "who can see it."
That question is tenant isolation, and it is where most of the actual engineering goes.
Propel is multi-tenant: many organizations share one database. The isolation is not a filter we remember to add in application code — that approach fails the first time someone forgets. It is enforced in the database itself with Postgres Row-Level Security. Every tenant table is scoped to an organization, and the policies decide what a given request may touch based on claims carried in the caller's JWT: their org_id, their role. Helper functions like jwt_org_id() and jwt_has_permission() encode those rules once, in SQL, so every table applies the same logic instead of each query reinventing it.
The important property of RLS is where it sits. It is enforced by the database on every query, no matter what path the query came in on — our internal API, a direct PostgREST call, anything. Application code can have a bug. A developer can forget a where clause. RLS is the backstop that does not depend on anyone remembering, because it runs below the code that might be wrong.
But — and this is the part teams get wrong in both directions — RLS is the last line of defense, not the only one. Treating it as the only line is how you end up leaking schema details in error messages or trusting a request body that quietly overrides ownership. So Propel validates input at the boundaries too: at the edge functions and the public API, before anything reaches the database. The two layers do different jobs. Boundary validation rejects malformed or malicious input early and returns clean errors. RLS guarantees that even if something slips past, the database still will not hand one tenant another tenant's rows. This is defense in depth, and the phrase is doing real work: you want to have to make more than one mistake before data crosses a tenant boundary.
A few design rules fall out of taking this seriously. Protected columns — the organization_id, the user_id, the ownership fields — are set on the server after any client-supplied data is applied, so a crafted payload cannot reassign a record to another org. Referenced IDs are checked for ownership at the boundary rather than trusting that RLS alone will sort out authorization. Realtime subscriptions are bound with a server-side organization filter, because a subscription scoped too broadly would stream every tenant's changes to every socket — a leak that is invisible until it is catastrophic. Raw database errors are never surfaced to public clients, because the shape of an error leaks the shape of your schema.
None of this is glamorous, and that is rather the point. Data residency done well is not a feature you demo. It is a set of quiet architectural commitments — host in the right place, isolate in the database, validate at the edges, and assume every single layer will eventually be asked to catch a mistake the layer above it made. If any one of those is treated as the whole answer, it isn't one.
For a CRM holding a company's entire commercial relationship graph, that is not over-engineering. It is the minimum a customer should be allowed to expect.