---
title: "Data and regions"
description: "Label personal data in the schema, keep each record in its owner's region (US, EU or India), and erase a person everywhere with a signed record."
section: "concepts"
canonical: "https://mainsdk.dev/docs/concepts/data-and-regions"
updated: "2026-10-28"
---

# Data and regions

> Label personal data in the schema, keep each record in its owner's region (US, EU or India), and erase a person everywhere with a signed record.

Canonical: https://mainsdk.dev/docs/concepts/data-and-regions · Updated: 2026-10-28

MainSDK can only protect data it knows about. So every field that holds personal data carries a label, every record lives in its owner's region, and one call exports or erases a person across every provider your app uses.

## Data labels

Labels come from one canonical set, used by every interface, driver, log and check:

| Label | For | Notes |
| --- | --- | --- |
| `personal` | Data about a person: email, phone, name | Redacted in logs and errors |
| `sensitive` | Special categories | Redacted in logs and errors |
| `child` | Data about minors | Redacted; shorter retention is typical |
| `phi` | Health information | Redacted; often `encrypt: true` |
| `pan` | Card numbers | Refused at check and at runtime: card numbers are never stored |

Any other label (`pii`, `health`, `payment`) fails validation.

```yaml title="main.yaml"
db:
  driver: postgres
  regions: { us: secret:DATABASE_URL_US, eu: secret:DATABASE_URL_EU, in: secret:DATABASE_URL_IN }
  fields:
    user.email:    { label: personal }
    user.phone:    { label: personal }
    patient.notes: { label: phi, encrypt: true }   # envelope encryption through keys
```

The check fails on a personal-looking field with no label (`MAIN-DATA-0021`) and on any `pan` field (`MAIN-DATA-0020`). Your App Store privacy labels, Play Data safety form and web privacy page are generated from these labels and your drivers, never written by hand:

```text
$ npx mainsdk privacy labels --format app-store
Data linked to you:
- Email Address (user.email; personal), linked to the user, for App Functionality

Processors that handle this data for the app:
- Resend (email)
```

## Three regions: US, EU and India

`db` keeps one store per region and writes each record to its owner's region, chosen by the market overlay.

```yaml title="main.yaml"
data:
  residency: { default: eu, by-market: { in: in, us: us } }
```

```ts title="app/api/orders/route.ts"
import { auth, db } from "mainsdk";

export async function POST(req: Request) {
  const session = await auth.require(req);
  // An EU user's order lands in the eu store; an India user's in the in store.
  const order = await db.orders.for(session.user).create(await req.json());
  return Response.json(order, { status: 201 });
}
```

- `mainsdk check` refuses queries that join across regions (`MAIN-DATA-0013`).
- Analytics combine regions only as totals.
- Moving a user between regions is an audited job, never a silent write.
- Drivers declare the regions they support. Residency values are limited to `us`, `eu` and `in`; markets resolve per request through [overlays](https://mainsdk.dev/docs/concepts/main-yaml).
- MainSDK Cloud runs in the same three regions, included on every plan at no extra cost.

> **Note:** Countries beyond the three regions use residency vault drivers (for example InCountry or Skyflow), which arrive in phase 3.

## Retention

Declare how long each label is kept. MainSDK carries it through every interface, driver, log, backup and analytics store. A label in use with no rule fails the check (`MAIN-PRIV-0001`).

```yaml title="main.yaml"
privacy:
  retention:
    personal: 365d
    sensitive: 365d
    child: 90d
    phi: 7y
    logs: 30d
    analytics: 13m
  erase: { confirm-within: 30d }
```

## Export and erasure

```ts title="app/actions/account.ts"
import { action, privacy } from "mainsdk";
import { s } from "mainsdk/schema";

export const deleteMyAccount = action({
  name: "account.delete",
  intent: "Erase the signed-in user's data everywhere",
  input: s.object({}),
  risk: "destructive",
  run: async ({ user }) => privacy.erase(user, { reason: "user request" }),
});
```

- `privacy.export(user)` returns one bundle with a JSON file per driver and a manifest of labels and regions.
- `privacy.erase(user)` fans out to every driver that holds the person's data, waits for each to confirm, retries those that fail, then writes an **erasure record signed by the project key** listing each driver, what it deleted and when.
- Data that can't be deleted at once, such as backups, is erased by deleting the person's own key. With `keys.per-user: true`, their rows in old backups no longer decrypt.
- Logs and analytics hold only pseudonymised IDs, so erasure there is a mapping delete.
- A legal hold (`privacy.hold`) pauses erasure and retention for that person and is audited.

```yaml title="main.yaml"
keys:
  driver: aws-kms
  per-user: true      # erase a person by deleting their key
  rotate: 365d
```

Consent is recorded per purpose. If you already run a consent platform, MainSDK reads its IAB TCF or GPP signal, with no second banner.

Full calls are on the [privacy](https://mainsdk.dev/docs/interfaces/privacy), [db](https://mainsdk.dev/docs/interfaces/db) and [keys](https://mainsdk.dev/docs/interfaces/keys) pages.

Still open: what happens when a third-party driver can't confirm erasure within `confirm-within`. The proposal is a partial record shown to admins while the job keeps retrying.

## Next

- [Security model](https://mainsdk.dev/docs/concepts/security-model)
- [Privacy and data rights](https://mainsdk.dev/docs/interfaces/privacy)
- [Cloud and regions](https://mainsdk.dev/docs/architecture/cloud-and-regions)
