gdpr
Overview
Practical GDPR/privacy for my B2C PWAs — not legal advice, an engineering checklist.
First classify the app's data posture, then run the decision gates, then apply the
guidance for the gaps. Security controls (RLS, secrets, open-write) live in security-bar —
referenced, not restated. This skill owns privacy/compliance: lawful basis, minimization,
retention, data-subject rights, processors, transfers, the privacy notice, and analytics.
When to use
- Building or shipping a feature that collects/stores/transmits personal data.
- Auditing an existing app for GDPR gaps, or writing its privacy notice.
- Choosing/adding analytics (read the analytics section first — it's coupled to consent).
Step 1 — classify the data posture
My fleet splits three ways; the posture decides how much applies:
- Offline / on-device (e.g. kartaak, converthub) — no server, no egress of user data.
Privacy by architecture. GDPR surface ≈ minimal; the win is stating it (a "runs on
your device, we collect nothing" notice = trust + the easy compliance win).
- Server-backed (e.g. EventSplit, Monete — Supabase) — I'm a controller. Personal
data of users and third parties (e.g. friends added to an event who never interacted
with me). This is the real surface: lawful basis, minimization, retention, DSR, RLS.
- Client-side but ships to third-party processors (e.g. mintza → OpenAI/Anthropic/
Google/Azure) — heaviest: multiple processors, international transfer (US), and
possibly special-category content. Controller/processor split depends on BYO-key vs
hosted — decide it explicitly.
Step 2 — decision gates
Run these on the data path (adapted from ECC's HIPAA gates):
- Is this personal data? (identifies a person directly or combined — name, email,
expense tied to a named person, voice). If no → stop, nothing here applies.
- Lawful basis? Name it: consent, contract, or legitimate interest (third-party names
in a shared event = legitimate interest; document it).
- Does this processor / model provider need a DPA before touching the data? (Supabase,
any analytics tool, any AI API.) International transfer covered (SCCs / adequacy / DPF)?
- Minimum necessary? Store the least that works (a display label, not full identity).
- DSR / erasure handled? Can a person get their data deleted? For no-login apps,
"delete the event" is the erasure path — make it real and reachable.
- Auditable? Only where proportionate — see the non-goal below.
Step 3 — guidance for the gaps
- Privacy notice — every app gets one (even offline: "we collect nothing"). Use
references/privacy-notice.md. Disclose every processor (Supabase, analytics, AI APIs)
and any transfer.
- Retention — cap growing personal data; EventSplit's history/trash caps already act as
de-facto retention — document them as the policy.
- Tag PII at the schema level (Supabase posture):
COMMENT ON COLUMN events.participant_name IS 'PII: name';
— makes personal-data columns explicit for audits and future migrations.
- Security controls (RLS, open-write, secrets, egress) →
eskills:security-bar. Note:
EventSplit's open-write RLS means anyone can read/delete personal data → that's a privacy
exposure, fix per security-bar.
Analytics — privacy-first, no consent banner
No tracking ⇒ no cookie banner. Don't add one you don't need. To get metrics and keep
that win:
- Use cookieless, no-PII, EU-hosted analytics — Plausible / Umami / PostHog-EU
(cookieless). No cookie/
localStorage identifier, no cross-site → no consent required.
- Do NOT use GA4 / Meta Pixel / anything cookie- or fingerprint-based → that requires
consent + a banner (the bloat we avoid).
- Per posture: offline apps can still ship cookieless pageviews; Supabase apps must disclose
the analytics tool as a processor; mintza already ships to AI, so cookieless analytics
adds negligible marginal exposure.
Never expose PII in… (guardrail list)
Never put personal data in: logs, analytics events, crash/error reports, LLM prompts,
URLs/query strings, localStorage/sessionStorage, or screenshots. Prefer opaque IDs
over names/email. Never ship the service_role key client-side (→ security-bar).
// BAD — leaks PII into the error (and the error tracker)
throw new Error(
`No member ${member.name} <${member.email}> in event ${event.title}`,
);
// GOOD — generic message; details logged server-side with opaque IDs only
logger.error("member lookup failed", { memberId: member.id, eventId });
throw new Error("Member not found");
For the AI-processor posture (mintza): send the model the minimum content needed, never
attach identifiers you don't have to, and disclose the provider + transfer in the notice.
Not in scope here
- Security controls (RLS, secrets, authz, egress) →
security-bar.
- Concrete failure recipes (open-RLS exposure, undisclosed transfer) →
stack-gotchas.
- Non-goal (arch-bar): no HIPAA-grade audit-everything, session-timeout, or
facility-RLS machinery — disproportionate for no-login B2C PWAs and fights minimization +
the egress limit. Audit only where proportionate.
- Scope is EU GDPR; UK-GDPR is near-identical; CCPA is out of scope.
1---2name: gdpr3description: Use when building, shipping, or auditing one of my B2C apps for GDPR/privacy — classify the data posture (offline / Supabase-backed / ships-to-AI-processor), run the decision gates (personal data? lawful basis? processor DPA? transfer? minimization? DSR/erasure?), add a privacy notice, and add privacy-first cookieless analytics without triggering consent. EU GDPR; references security-bar for the controls.4---56# gdpr78## Overview910Practical GDPR/privacy for my B2C PWAs — **not legal advice**, an engineering checklist.11First classify the app's **data posture**, then run the **decision gates**, then apply the12guidance for the gaps. Security controls (RLS, secrets, open-write) live in `security-bar` —13referenced, not restated. This skill owns privacy/compliance: lawful basis, minimization,14retention, data-subject rights, processors, transfers, the privacy notice, and analytics.1516## When to use1718- Building or shipping a feature that collects/stores/transmits personal data.19- Auditing an existing app for GDPR gaps, or writing its privacy notice.20- Choosing/adding analytics (read the analytics section first — it's coupled to consent).2122## Step 1 — classify the data posture2324My fleet splits three ways; the posture decides how much applies:25261. **Offline / on-device** (e.g. kartaak, converthub) — no server, no egress of user data.27 **Privacy by architecture.** GDPR surface ≈ minimal; the win is _stating_ it (a "runs on28 your device, we collect nothing" notice = trust + the easy compliance win).292. **Server-backed** (e.g. EventSplit, Monete — Supabase) — I'm a **controller**. Personal30 data of users _and third parties_ (e.g. friends added to an event who never interacted31 with me). This is the real surface: lawful basis, minimization, retention, DSR, RLS.323. **Client-side but ships to third-party processors** (e.g. mintza → OpenAI/Anthropic/33 Google/Azure) — heaviest: multiple **processors**, **international transfer (US)**, and34 possibly **special-category** content. Controller/processor split depends on BYO-key vs35 hosted — decide it explicitly.3637## Step 2 — decision gates3839Run these on the data path (adapted from ECC's HIPAA gates):40411. **Is this personal data?** (identifies a person directly or combined — name, email,42 expense tied to a named person, voice). If no → stop, nothing here applies.432. **Lawful basis?** Name it: consent, contract, or legitimate interest (third-party names44 in a shared event = legitimate interest; document it).453. **Does this processor / model provider need a DPA** before touching the data? (Supabase,46 any analytics tool, any AI API.) International transfer covered (SCCs / adequacy / DPF)?474. **Minimum necessary?** Store the least that works (a display label, not full identity).485. **DSR / erasure handled?** Can a person get their data deleted? For no-login apps,49 "delete the event" _is_ the erasure path — make it real and reachable.506. **Auditable?** Only where proportionate — see the non-goal below.5152## Step 3 — guidance for the gaps5354- **Privacy notice** — every app gets one (even offline: "we collect nothing"). Use55 `references/privacy-notice.md`. Disclose every processor (Supabase, analytics, AI APIs)56 and any transfer.57- **Retention** — cap growing personal data; EventSplit's history/trash caps already act as58 de-facto retention — document them as the policy.59- **Tag PII at the schema level** (Supabase posture): `COMMENT ON COLUMN events.participant_name IS 'PII: name';`60 — makes personal-data columns explicit for audits and future migrations.61- **Security controls** (RLS, open-write, secrets, egress) → `eskills:security-bar`. Note:62 EventSplit's open-write RLS means anyone can read/delete personal data → that's a privacy63 exposure, fix per security-bar.6465## Analytics — privacy-first, no consent banner6667**No tracking ⇒ no cookie banner. Don't add one you don't need.** To get metrics _and_ keep68that win:6970- **Use cookieless, no-PII, EU-hosted analytics** — Plausible / Umami / PostHog-EU71 (cookieless). No cookie/`localStorage` identifier, no cross-site → **no consent required.**72- **Do NOT use** GA4 / Meta Pixel / anything cookie- or fingerprint-based → that _requires_73 consent + a banner (the bloat we avoid).74- Per posture: offline apps can still ship cookieless pageviews; Supabase apps must disclose75 the analytics tool as a **processor**; mintza already ships to AI, so cookieless analytics76 adds negligible marginal exposure.7778## Never expose PII in… (guardrail list)7980Never put personal data in: **logs, analytics events, crash/error reports, LLM prompts,81URLs/query strings, `localStorage`/`sessionStorage`, or screenshots.** Prefer **opaque IDs**82over names/email. Never ship the `service_role` key client-side (→ `security-bar`).8384```ts85// BAD — leaks PII into the error (and the error tracker)86throw new Error(87 `No member ${member.name} <${member.email}> in event ${event.title}`,88);89// GOOD — generic message; details logged server-side with opaque IDs only90logger.error("member lookup failed", { memberId: member.id, eventId });91throw new Error("Member not found");92```9394For the AI-processor posture (mintza): send the model the **minimum** content needed, never95attach identifiers you don't have to, and disclose the provider + transfer in the notice.9697## Not in scope here9899- Security controls (RLS, secrets, authz, egress) → **`security-bar`**.100- Concrete failure recipes (open-RLS exposure, undisclosed transfer) → **`stack-gotchas`**.101- **Non-goal (arch-bar):** no HIPAA-grade _audit-everything_, session-timeout, or102 facility-RLS machinery — disproportionate for no-login B2C PWAs and fights minimization +103 the egress limit. Audit only where proportionate.104- Scope is **EU GDPR**; UK-GDPR is near-identical; CCPA is out of scope.