Data residency
Requires a license with the
data-residencyfeature. See pricing.
Data residency records where each tenant's data must live and holds every engine to it. You define the regions you run in, assign each tenant to one, and start each engine with the region it serves. A write from a tenant that belongs in another region is refused and told where to go.
How enforcement works
Section titled “How enforcement works”When an engine starts with INSTANCE_REGION set, the Content API checks every
write (any method except GET, HEAD and OPTIONS):
| Tenant | Result |
|---|---|
| No region assigned | Writes anywhere. |
| Assigned to this engine's region | Writes as usual. |
| Assigned to another region | 421 Misdirected Request. |
| Region cannot be read | 503 with cannot determine the tenant's data region. The write is never let through. |
HTTP/1.1 421 Misdirected RequestX-CMS-Region: us-east-1X-CMS-Region-Target: eu-west-1X-CMS-Region-Reason: data-residency-enforced
{"error":"write routed to wrong region","target_region":"eu-west-1"}Route the request to an engine in X-CMS-Region-Target. Reads are not checked,
and neither is the Admin API. Without INSTANCE_REGION, nothing is enforced.
With it, every Content API response carries X-CMS-Region with this engine's
region, so a load balancer can route on it.
Residency decides where a tenant's writes land. It does not look at where a
client connects from.
Uploads follow the same assignment. A tenant's files go only to storage
providers in its region, and an upload whose region cannot be read answers
503 rather than land elsewhere. See
object storage.
Try it
Section titled “Try it”This defines a region, assigns a tenant to it and reads the report. You need a
super admin token in TOKEN. The
quickstart shows how to get one.
-
Create a region:
Terminal window curl -X POST http://localhost:3001/api/admin/regions \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d '{"slug": "eu-west-1", "display_name": "EU West (Ireland)", "provider": "aws", "coordinates": {"lat": 53.35, "long": -6.26}}'The answer is
201with the region. Copy itsidintoREGION. -
Find the tenant's id with
GET /api/admin/tenantsand copy it intoTENANT. A tenant is assigned by its id, not its slug. -
Assign the tenant:
Terminal window curl -X POST http://localhost:3001/api/admin/tenants/$TENANT/region \-H "Authorization: Bearer $TOKEN" \-H "Content-Type: application/json" \-d "{\"region_id\": \"$REGION\"}"{"id": "909c7ca6-f9ed-4445-82bb-51f62b836bd7","tenant_id": "3f64a521-1146-481c-aaf8-a3334f505aa3","region_id": "76e7b309-80f4-4341-a958-2b15dacbbe44","region_slug": "eu-west-1","assigned_at": "2026-10-02T09:14:56Z"} -
Read the report:
Terminal window curl http://localhost:3001/api/admin/residency/report \-H "Authorization: Bearer $TOKEN"It lists
regions, every tenant's assignment undertenant_regions, and asummary:{"total_regions": 1,"total_tenants": 1,"tenants_per_region": {"eu-west-1": 1},"replicating_count": 0,"unassigned_count": 0} -
Ask for the nearest region the way a sign-up page would, with no credentials:
Terminal window curl -X POST http://localhost:3002/api/v1/regions/nearest \-H "Content-Type: application/json" \-d '{"lat": 48.85, "long": 2.35}'The answer is the region with
distance_km, about781from Paris to Dublin. -
Open Settings > Data residency in the admin console. The region and the assignment are there.
Turn it on
Section titled “Turn it on”- Run with a license that includes
data-residency. If you setLYEVE_PLUGINS, includedata-residencyin it. See Licensing and tiers. - On each engine, set
INSTANCE_REGIONto the slug of the region it serves. - Create regions and assign tenants, in the console or over the API.
Without the license the feature does not start, and its routes answer 404.
Manage regions
Section titled “Manage regions”Regions need a super admin. slug and display_name are required, and
provider defaults to aws. coordinates lets the nearest-region lookup use
the region. is_default marks the default region, and marking one clears the
mark from the others. PUT also takes enabled. Only enabled regions are
listed publicly.
An admin can read and set their own tenant's region. A super admin can do it for
any tenant. GET /api/admin/tenants/{id}/region returns the assignment, or 404
with no region assigned.
Record replicas and planned moves
Section titled “Record replicas and planned moves”These record your disaster-recovery setup and planned moves between regions. The engine stores them and shows them in the report. Copying the data is up to you.
POST /api/admin/tenants/{id}/replicationtakesreplica_region_id,target_rpo_secs(default3600) andtarget_rto_secs(default1800), and answers201with a record in statussyncing.PUT /api/admin/tenants/{id}/replication/{rid}updatesstatus, one ofsyncing,synced,failedorpaused, and the lag.POST /api/admin/tenants/{id}/migratetakesto_region_idand an optionalrequested_by, and answers202with a migration record in statusestimating.
Public region lookup
Section titled “Public region lookup”Two Content API routes need no credentials:
GET /api/v1/regionslists enabled regions, paginated (limitdefault 50,offset). 30 requests a second per address, bursts of 50.POST /api/v1/regions/nearesttakes{"lat": ..., "long": ...}and answers{"region": {...}, "distance_km": ...}. 10 requests a second per address, bursts of 20.
Settings
Section titled “Settings”| Variable | What it does | Default |
|---|---|---|
INSTANCE_REGION | The region slug this engine serves. Set, it turns on write enforcement. | unset |
Routes
Section titled “Routes”Regions, assignments and records
| Method | Path | Who |
|---|---|---|
GET, POST | /api/admin/regions | super admin |
GET, PUT, DELETE | /api/admin/regions/{id} | super admin |
GET, POST | /api/admin/tenants/{id}/region | admin of that tenant |
GET, POST | /api/admin/tenants/{id}/replication | admin of that tenant |
PUT, DELETE | /api/admin/tenants/{id}/replication/{rid} | admin of that tenant |
POST | /api/admin/tenants/{id}/migrate | admin of that tenant |
GET | /api/admin/tenants/{id}/migrations | admin of that tenant |
GET | /api/admin/tenants/{id}/migrations/{mid} | admin of that tenant |
GET | /api/admin/residency/report | super admin |
GET | /api/v1/regions | anyone |
POST | /api/v1/regions/nearest | anyone |
Errors
Section titled “Errors”| Status | Message | Cause |
|---|---|---|
421 | write routed to wrong region | The tenant belongs in another region. |
403 | forbidden: super_admin required | A tenant admin called a region or report route. |
403 | forbidden: cannot access another tenant's data | A tenant admin named another tenant. |
Every error
| Status | Message | Cause |
|---|---|---|
400 | slug is required, display_name is required | Region created without them. |
400 | region_id is required, to_region_id is required | Assignment or migration without a region. |
400 | invalid status: must be one of syncing, synced, failed, paused | Unknown replication status. |
404 | region not found, no region assigned, replication not found, migration not found | Wrong id. |
409 | region with slug "<slug>" already exists | Duplicate slug. |
503 | cannot determine the tenant's data region | The tenant's region could not be read during a write. |
Related
Section titled “Related”- Tenants: what a tenant is and how a request picks one.
- PII masking: keep personal data out of responses and logs.
- Data protection: how residency fits a vendor review.