MainSDK
Search⌘K
Docs

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.

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:

LabelForNotes
personalData about a person: email, phone, nameRedacted in logs and errors
sensitiveSpecial categoriesRedacted in logs and errors
childData about minorsRedacted; shorter retention is typical
phiHealth informationRedacted; often encrypt: true
panCard numbersRefused at check and at runtime: card numbers are never stored

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

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.

main.yaml
data:
  residency: { default: eu, by-market: { in: in, us: us } }
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.
  • 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).

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

Export and erasure

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.
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, 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?

↑↓ moveEnter openEsc close

Not decided yet