MainSDK
Search⌘K
Integrations

AWS KMS

Use AWS KMS for MainSDK encryption keys - one wrapping key per region, IAM permissions, rotation, main.yaml, local testing and swapping.

1 interface1 driverWritten guide
Updated 2026-10-28

AWS Key Management Service (KMS) is the default driver behind keys. MainSDK encrypts each record or file with its own data key, and AWS KMS wraps that data key with a key that never leaves KMS. You give MainSDK one KMS key per region, so a record is always wrapped by its owner's region key. Pick it when your app runs on AWS, or when you have no reason to pick another key service.

Interfaces it serves

Pick AWS KMS in main.yaml for any of these. Your calls stay the same, and you can swap later.

Before you start

You need an AWS account with KMS available in the regions you serve.

  1. Create one symmetric encryption key per MainSDK region. KMS keys are regional. The catalog example uses us-east-1 for US, eu-west-1 for EU and ap-south-1 for India; use the regions your app and data live in. An alias per key (such as alias/pinecart-eu) keeps main.yaml readable.
  2. Give your app an IAM role. Prefer workload identity: the role your compute already assumes. KMS credentials must come from workload identity or the secrets driver, never from code.
  3. Grant only what envelope encryption needs. Allow the role to generate and decrypt data keys with these keys, in the key policy or an IAM policy (in AWS terms, actions such as kms:GenerateDataKey and kms:Decrypt). The exact action list the driver needs is not specified yet. Keep key administration (scheduling deletion, changing policy) with a separate admin role.
  4. Decide on rotation. MainSDK rotates on the rotate period you set. AWS KMS can also rotate key material on its own schedule. How the two interact is not specified yet; avoid turning both on until it is.

Configure

main.yaml
keys:
  driver: aws-kms
  key:                       # one wrapping key per region
    us: arn:aws:kms:us-east-1:111122223333:alias/pinecart-us
    eu: arn:aws:kms:eu-west-1:111122223333:alias/pinecart-eu
    in: arn:aws:kms:ap-south-1:111122223333:alias/pinecart-in
  per-user: true             # a key per person, for crypto-erasure
  rotate: 365d               # automatic rotation period

Regions are us, eu and in. Keep the local driver for development in an overlay; mainsdk check refuses it in production with MAIN-KEYS-0001.

main.env.dev.yaml
keys:
  driver: local

Use it

Most of the time you don't call keys directly: db fields with encrypt: true, files and crypto use it for you. To seal and open a value yourself:

app/api/notes/route.ts
import { auth, keys } from "mainsdk";

export async function POST(req: Request) {
  const session = await auth.require(req);
  const { note } = await req.json();
  const sealed = await keys.encrypt(note, { owner: session.user, region: "eu", label: "phi" });
  // { ciphertext, wrappedKey, keyId, alg: "AES-256-GCM" }
  const plain = await keys.decrypt(sealed, { purpose: "echo note to owner" });
  return Response.json({ ok: plain.byteLength > 0 });
}

Erase a person everywhere, backups included, with keys.destroyUser(user) when per-user is on. Rotate on demand with keys.rotate() or keys.rotate({ keyId }).

Local development and testing

  • No AWS account: the local driver seals and opens values on your machine, for development only.
  • KMS without AWS: LocalStack emulates KMS, and MainSDK's keys conformance suite runs the aws-kms driver against it.
  • Tests: createTestApp() from @mainsdk/testing runs keys on its in-memory fake, and chaos.on("keys", { outage: true }) simulates KMS being unreachable.
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({ main: "./main.yaml" });
  const user = { id: "u1" };
  const sealed = await keys.encrypt("note", { owner: user, region: "eu", label: "phi" });
  await keys.destroyUser(user);
  await assert.rejects(keys.decrypt(sealed, { purpose: "test" }), { code: "MAIN-KEYS-0004" });
});

What MainSDK adds on top

  • A data key per record (AES-256-GCM or XChaCha20-Poly1305), wrapped by a KMS key that never leaves KMS.
  • One wrapping key per region, chosen from the record owner's region.
  • Every wrap, unwrap, destroy and rotate written to the audit log with key ID, purpose and caller, never the key material.
  • Automatic rotation with old data still readable, rewrapped on its next write or by a background job.
  • Per-user keys, so keys.destroyUser() erases a person's data everywhere.

See Encryption keys for the full contract.

Limits and notes

  • The driver comes with the keys spec (phase 1) and is the default for keys.
  • A KMS key belongs to one AWS region. Each MainSDK region needs its own key, in an AWS region that fits your data residency.
  • Sealing with the label pan is refused, because card numbers are never stored.
  • Hardware security modules (including AWS CloudHSM) and customer-held keys through AWS XKS come with the hsm-keys spec in phase 3.
  • keys.raw() returns the KMS client for features keys does not wrap. mainsdk check lists every use.
  • On the Fleet plan, MainSDK Cloud can rotate or revoke a key everywhere during an incident; everything above is free in your app.

Swap to or from it

main.yaml
keys:
  driver: gcp-kms   # was aws-kms; point key.us, key.eu and key.in at the new service's keys

FAQ

Can I use one multi-region key for every region? MainSDK asks for one wrapping key per region, set under key.us, key.eu and key.in, so each record stays tied to its owner's region.

Does destroying a user's key delete a KMS key? keys.destroyUser() returns a receipt with the destroyed key ID. How per-user keys map onto KMS resources is not specified yet.

Can I read old data after rotation? Yes. Old versions stay usable for decrypt, and records are rewrapped on their next write.

Next

Open questions

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet