Skip to content

PII, legal text, and GDPR runtime

Show Pony collects guest names and optional emails on the public RSVP form. That is personal data, but this tutorial app deliberately splits two concerns that newcomers often conflate:

ConcernWhat it isShow Pony chapter
Legal disclosureStatic Impressum / Privacy / Terms on the apex13: Legal pages
Operational GDPRExport, erasure, retention, restriction for live dataThis chapter + user-data-rights demo

Mira shares a link; guests type a name (required) and sometimes an email (optional, for the confirmation mail from chapter 10). Those rows land in the rsvp entity scoped to her tenant.

What she needs in production:

  1. Privacy policy that describes what is collected. Legal pages cover it.
  2. Technical protection for guest PII at rest (encryption / KMS).
  3. Data-subject rights. A guest may ask “delete my RSVP” or “send me everything you stored about me”.

Show Pony wires (2) and (3) into the RSVP path: guest fields are personal: "self" (encryptable at rest, forgettable per row) and the crypto-shredding feature is mounted with a forget-subject command, so a guest can be erased without a user account, once a KMS is provisioned. Both are KMS-gated: the demo boots in an explicit plaintext mode by default (see below), and forget-subject refuses to run without a configured KMS adapter. Only (1) is unconditionally live out of the box. It does not mount the full user-data-rights export/forget pipeline either. That would distract from the RSVP tutorial and lives in the dedicated sample instead.

Alex RSVP’d with name + email, then emails Mira: “Please remove my data.” Without a user account, there is no self-service “Delete my account” button. Mira (or your support flow) triggers erasure by calling crypto-shredding:write:forget-subject against the rsvp row. Because guest fields are personal: "self", that one call makes the ciphertext permanently unreadable. No per-column backfill or manual redaction is needed. This requires a KMS to be provisioned first: the demo’s default plaintext mode has no key to shred, and forget-subject returns an error without a configured adapter.

Wiring the self-service side of this (a magic-link “forget me” flow, export ZIP) is real product work (entity extensions, retention cron, audit) and lives in user-data-rights-demo, where that story is the point of the app.

Guest fields are personal: "self" so subject-key KMS can encrypt them at rest and crypto-shredding can erase the key later. find stays allowed on an annotated field. Only sortable is forbidden:

name: createTextField({
required: true,
personal: "self",
find: "fuzzy",
}),
email: createTextField({
personal: "self",
find: "exact",
format: "email",
}),
From src/features/show-pony/schema/rsvp.ts.

That is why the guest-list screen sorts by status instead of name. find: "fuzzy" still finds a guest by name, it just can’t paginate by it:

defaultSort: { field: "status", dir: "asc" },
From src/features/show-pony/register/screens.ts.

Production bootstrap wires a real KMS when one is configured, and falls back to an explicit, logged plaintext mode otherwise, never a silent one:

const kmsWiring = resolveKmsWiring(process.env, {
logPrefix: "[show-pony]",
plaintextReason: "show-pony demo app, no subject-keys KMS provisioned",
});
if ("allowPlaintextPii" in kmsWiring) {
console.warn(`[show-pony] PII IS STORED IN PLAINTEXT, ${kmsWiring.allowPlaintextPii}`);
}
From bin/main.ts.

Why this boots without a KMS in dev: resolveKmsWiring only returns a real kms/blindIndexKey pair when PLATFORM_KEK, SUBJECT_KEYS_DATABASE_URL, and KUMIKO_BLIND_INDEX_KEY are all set; otherwise it returns allowPlaintextPii with your reason string. A partial trio is a boot error, so there is no silent half-encrypted state.

Chapter 13 mounts composePagesStack, public HTML from seeded text-content blocks. This satisfies transparency (Impressum, Datenschutzerklärung) but does not register export/forget jobs.

Operational GDPR lives in bundled features:

  • compliance-profiles, region profile (e.g. eu-dsgvo)
  • data-retention, retention policies per entity
  • user-data-rights, Art. 15/17/18/20 pipeline
  • user-data-rights-defaults, stock hooks for user + file refs

The runnable reference app is user-data-rights-demo, todos + files + export ZIP + forget cron. Wire it with composeGdprStack() from @cosmicdrift/kumiko-dev-server/compose-stacks (see that app’s run-config.ts).

  1. KMS: provision PLATFORM_KEK, SUBJECT_KEYS_DATABASE_URL, and KUMIKO_BLIND_INDEX_KEY (or createPgKmsAdapter(...) directly) so resolveKmsWiring picks a real KMS instead of the plaintext fallback, the The personal: "self" fields and crypto-shredding feature are already wired.
  2. composeGdprStack() + user-data-rights in buildAppFeatures(...) : self-service export/forget for guests without a user account.
  3. r.useExtension(EXT_USER_DATA, "rsvp", { export, delete }), two hooks per entity; the framework runs export ZIP + forget cleanup (see user-data-rights recipe).
  4. Process, guest erasure may be email-based (no user account); hosts need an admin action or magic-link flow, design that in your domain, not in the tutorial.

Show Pony ships legal text unconditionally today, plus guest PII that is encryption-ready and forgettable via crypto-shredding once a KMS is provisioned. Copy the export/self-service half of the GDPR stack from the sample when Mira goes live.

Back to the tutorial index: Show Pony.