---
title: "Honour consent, export and erasure requests"
description: "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."
section: "guides"
status: "Building for 0.3"
spec: "privacy-lifecycle"
canonical: "https://mainsdk.dev/docs/guides/honour-consent-export-and-erasure"
updated: "2026-10-28"
---

# 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.

Status: Building for 0.3 · Canonical: https://mainsdk.dev/docs/guides/honour-consent-export-and-erasure · 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

```yaml title="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.

## 2. Record consent and gate tracking

```ts title="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

```ts title="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

```ts title="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:

```ts title="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

| Code | What it means | Fix |
| --- | --- | --- |
| `MAIN-PRIV-0001` | A label in use has no retention rule | Add `privacy.retention.<label>` |
| `MAIN-PRIV-0002` | A missing or malformed user id, reason, purpose or date | Pass what the call needs |
| `MAIN-PRIV-0003` | The person is under a legal hold | Release the hold when the case ends |
| `MAIN-PRIV-0004` | The `privacy` section does not read | Fix the key or duration the finding names |
| `MAIN-PRIV-0005` | A consent string is not TCF v2 or GPP | Pass 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

- [Seal data and erase a person with keys](https://mainsdk.dev/docs/guides/erase-a-user-with-keys)
- [Send engagement reminders](https://mainsdk.dev/docs/guides/engagement-messages)
- [Make your app enterprise ready](https://mainsdk.dev/docs/guides/enterprise-ready)
