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:
| Concern | What it is | Show Pony chapter |
|---|---|---|
| Legal disclosure | Static Impressum / Privacy / Terms on the apex | 13: Legal pages |
| Operational GDPR | Export, erasure, retention, restriction for live data | This chapter + user-data-rights demo |
User story: Mira’s guest list
Section titled “User story: Mira’s guest list”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:
- Privacy policy that describes what is collected. Legal pages cover it.
- Technical protection for guest PII at rest (encryption / KMS).
- 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.
User story: a guest asks to be forgotten
Section titled “User story: a guest asks to be forgotten”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.
What the schema does today
Section titled “What the schema does today”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",}),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" },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}`);}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.
Legal pages ≠ GDPR pipeline
Section titled “Legal pages ≠ GDPR pipeline”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 entityuser-data-rights, Art. 15/17/18/20 pipelineuser-data-rights-defaults, stock hooks foruser+ 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).
What you would add for a real launch
Section titled “What you would add for a real launch”- KMS: provision
PLATFORM_KEK,SUBJECT_KEYS_DATABASE_URL, andKUMIKO_BLIND_INDEX_KEY(orcreatePgKmsAdapter(...)directly) soresolveKmsWiringpicks a real KMS instead of the plaintext fallback, the Thepersonal: "self"fields andcrypto-shreddingfeature are already wired. composeGdprStack()+user-data-rightsinbuildAppFeatures(...): self-service export/forget for guests without a user account.r.useExtension(EXT_USER_DATA, "rsvp", { export, delete }), two hooks per entity; the framework runs export ZIP + forget cleanup (see user-data-rights recipe).- 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.