MainSDK
Search⌘K
How-to

Honour consent, export and erasure requests

Declare retention per data label, record consent, read a consent platform's string, export a person's data and erase it with a signed record.

Building for 0.3

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

You will declare how long each kind of data is kept, record a consent, export one person's data and erase it. It takes about 25 minutes.

You need

  • A MainSDK app with db and labelled fields (personal, sensitive, child or phi).
  • For erasing backups, keys with per-user: true.

1. Declare retention for every label in use

main.yaml
privacy:
  retention:
    personal: 365d
    sensitive: 365d
    logs: 30d
    analytics: 13m
  erase: { confirm-within: 30d, retries: 3, retry-delay: 1s }
  consent:
    tcf: { analytics: [1], marketing: [1, 2, 7] }

A label in use with no rule fails check with MAIN-PRIV-0001. In retention, a bare m is months, so 13m is thirteen months, never minutes.

app/lib/consent.ts
import { privacy } from "mainsdk";

export async function onBanner(user: string, analytics: boolean) {
  await privacy.consent(user).set({ purpose: "analytics", granted: analytics });
}

export async function onTcfString(user: string, tcf: string) {
  await privacy.consent(user).signal({ tcf });        // no second banner
}

export function track(user: string, send: () => void) {
  return privacy.guard(user, { purpose: "analytics", send });   // sends only on consent
}

consent(user).get(purpose) is false until the person, or a consent platform's signal, has said yes. A string that is not TCF v2 or GPP is refused with MAIN-PRIV-0005.

3. Export

app/api/me/export/route.ts
import { privacy } from "mainsdk";

declare const userId: string;   // the signed-in person, from auth.require

const bundle = await privacy.export(userId);   // one JSON file per driver, plus a manifest

The manifest lists each file's labels and region.

4. Erase

app/api/me/erase/route.ts
import { privacy } from "mainsdk";

declare const userId: string;   // the signed-in person, from auth.require

const record = await privacy.erase(userId, { reason: "user request" });
privacy.verify(record);                         // the signature checks against the project key

Erase fans out to every driver that holds the person's data and retries the ones that fail. If a driver has not confirmed after its retries, the record is partial (MAIN-PRIV-0006, logged, never thrown) and privacy.resume() retries. A person under a legal hold, privacy.hold(user, { until, reason }), is refused with MAIN-PRIV-0003; privacy.release(user) lifts it.

Register anything of your own that holds a person's data:

app/lib/privacy.ts
import { privacy } from "mainsdk";

declare const notes: { deleteFor(user: string): Promise<void>; dump(user: string): Promise<unknown> };   // your own notes store

privacy.register("support-notes", (user) => notes.deleteFor(user), { export: (user) => notes.dump(user) });

5. Generate store labels

bash
npx mainsdk privacy labels --format app-store
npx mainsdk privacy labels --format play

Labels come from your data labels and drivers, never by hand.

Verify

bash
npx mainsdk check

Exit 0 means every label in use has a retention rule and the privacy section reads.

If it fails

CodeWhat it meansFix
MAIN-PRIV-0001A label in use has no retention ruleAdd privacy.retention.<label>
MAIN-PRIV-0002A missing or malformed user id, reason, purpose or datePass what the call needs
MAIN-PRIV-0003The person is under a legal holdRelease the hold when the case ends
MAIN-PRIV-0004The privacy section does not readFix the key or duration the finding names
MAIN-PRIV-0005A consent string is not TCF v2 or GPPPass the string the platform gave you

For agents

text
Goal: add retention, consent and an erase route for personal data.
Touch: main.yaml (privacy) and the routes that export and erase.
Do not: write your own consent table, delete script or label list; skip a label's retention rule.
Verify: npx mainsdk check; erase a test user and privacy.verify the record.
Stop and ask if: a retention period is a legal question, or a module holds a person's data with no erase hook.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet