MainSDK
Search⌘K
How-to

Seal data and erase a person with keys

Seal a value with a per-user key held in your cloud KMS, open it with an audited purpose, rotate keys, and destroy one person's key to erase them everywhere.

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 seal a note for one user, open it, then destroy that user's key so the note can never be read again. It takes about 20 minutes. Most apps never call keys directly: a db field with encrypt: true uses it for you. Use it when you seal a value yourself or need crypto-erasure.

You need

  • A MainSDK app with auth.
  • For production, a KMS key per region. In development the local driver needs no account.

1. Point keys at the KMS

main.yaml
keys:
  driver: aws-kms
  key:
    us: arn:aws:kms:us-east-1:111122223333:alias/pinecart-us
    eu: arn:aws:kms:eu-west-1:111122223333:alias/pinecart-eu
  per-user: true
  rotate: 365d

The local driver is refused in production with MAIN-KEYS-0001.

2. Seal a value

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

export async function sealNote(note: string, userId: string) {
  return keys.encrypt(note, { owner: userId, region: "eu", label: "phi" });
}

The label is one of personal, sensitive, child or phi.

3. Open it with a purpose

app/lib/open-note.ts
import { keys } from "mainsdk";
import type { Sealed } from "mainsdk/keys";

export async function openNote(sealed: Sealed) {
  return keys.decrypt(sealed, { purpose: "show note to owner" });   // Uint8Array
}

The purpose and key id go to the audit log.

4. Rotate

server/rotate.ts
import { keys } from "mainsdk";
import type { Sealed } from "mainsdk/keys";

export async function rotate(sealed: Sealed) {
  const report = await keys.rotate();                 // or keys.rotate({ keyId: "user:u1" })
  const moved = await keys.rewrap(sealed, { purpose: "move to the new key version" });
  return { report, moved };
}

Old data still decrypts after a rotation. rewrap wraps the data key again and leaves the ciphertext alone.

5. Erase a person

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

export async function erase(id: string) {
  return keys.destroyUser(id);   // { keyId, scope, destroyedAt, versions }
}

6. Test it

app/lib/notes.test.ts
import { it } from "node:test";
import assert from "node:assert/strict";
import { createTestApp } from "@mainsdk/testing";
import { keys } from "mainsdk";

it("keys AC3 makes a destroyed user's data unreadable", async () => {
  createTestApp({ yaml: "schema: 1\nproject: prj_test\nkeys:\n  driver: local\n  per-user: true\n" });
  const sealed = await keys.encrypt("note", { owner: "u1", region: "eu", label: "phi" });
  await keys.destroyUser("u1");
  await assert.rejects(keys.decrypt(sealed, { purpose: "test" }), { code: "MAIN-KEYS-0004" });
});

Check it worked

The test passes, and reading the note after destroyUser fails with MAIN-KEYS-0004.

Troubleshooting

  • MAIN-KEYS-0003: a bad region, no KMS key for the region, or decrypt without a purpose.
  • MAIN-KEYS-0005: the key service did not answer. Check the app may use the KMS key, then retry.
  • MAIN-KEYS-0006: the sealed value failed its integrity check. It was changed or belongs to another key.

See the keys interface.

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet