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.
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.
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 keysThe 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:
$ 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.
data:
residency: { default: eu, by-market: { in: in, us: us } }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 checkrefuses 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,euandin; markets resolve per request through overlays. - MainSDK Cloud runs in the same three regions, included on every plan at no extra cost.
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).
privacy:
retention:
personal: 365d
sensitive: 365d
child: 90d
phi: 7y
logs: 30d
analytics: 13m
erase: { confirm-within: 30d }Export and erasure
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.
keys:
driver: aws-kms
per-user: true # erase a person by deleting their key
rotate: 365dConsent 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, db and 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
Was this page helpful?