FADP Posture¶
This page is the rules-layer contract for RBO's posture under Switzerland's Federal Act on Data Protection (FADP, in force since 2023-09-01). It covers the data-subject rights surfaces that the client identity & sharing page and the stringer lifecycle page do not already pin down: data-subject access (DSAR), right to erasure, right to data portability, retention windows, consent records, and processor / sub-processor disclosure. It also specifies the skeleton of the public-facing privacy notice (the actual notice copy is a legal-review deliverable — out of scope here).
Cross-cuts: V2 scope, client identity & sharing (audit + share grants are the de-facto consent log), stringer lifecycle (stringer-side scrub on offboarding), data model, order lifecycle (receipt-emit log retention). Architectural counterpart: ADR-0004 (Theo) names granter_kind on every share grant — that is the consent-trail backbone this page leans on. Tracked in racket-book#98.
What this page does NOT cover. It does not specify the schema for
Person.deleted_at,Person.scrubbed_at, or thedsar_logtable — those are flagged here as schema asks for Theo (data-model amendment) and implementation asks for Pax (next-slice endpoint work). It does not write the public-facing privacy notice — that is a legal-review deliverable; this page only specifies the skeleton.
Scope of FADP applicability¶
- Personal data subjects RBO processes:
- Stringers — onboarded by admin; identifiable by email, business-identity fields, etc.
- Persons (clients) — identified by display name + (optional) verified email. Includes draft Persons created by stringers on behalf of not-yet-onboarded clients.
- Lawful bases asserted by RBO, by data class (cross-references the retention policy):
- Contract performance — Order rows, receipt-emit log, receipt PDFs (the platform is delivering the stringing-job tracking + receipt service to the stringer; the stringer in turn is delivering the stringing service to their client).
- Legal obligation — financial / business-record retention per Swiss Code of Obligations art. 957–958f (10-year minimum for books and supporting documents; receipts qualify as supporting documents to the stringer's bookkeeping).
- Legitimate interest — audit trail (
share_audit,Order.receipt_emit_log) needed to demonstrate FADP compliance itself; security logs. - Consent — Person notification preferences (in-app mandatory baseline; email opt-in per CC-2026-05-02-2). Stringer-created draft Persons are consent-by-proxy — see Consent records.
Right to access (DSAR)¶
A Person (or their legal guardian) is entitled to ask RBO what personal data the platform holds about them, in an intelligible form.
Surface¶
- V2 — admin-mediated. Persons send a request by email or other out-of-band channel; the admin (Stefan) triggers an "export DSAR for Person
" admin action that runs the export pipeline below and emails the result back. - V3 — self-service. The V3 client-facing portal exposes a "Download my data" affordance. Identity is asserted by the same magic-link flow that gates the rest of the portal. Per V3 vision — C1.
What's in the DSAR response¶
A single JSON document (the canonical FADP-grade format) containing:
| Section | Contents |
|---|---|
person |
The Person row's public fields: id, display_first_name, display_last_name, email (if set), email_verified_at, default_locale, notification_prefs, created_at, updated_at, deleted_at (if soft-deleted), scrubbed_at (if hard-erased — should be empty in any DSAR served, since a scrubbed Person has no PII left to disclose). |
client_profiles[] |
One entry per stringer who has a ClientProfile for this Person. Each entry lists: stringer display name (so the Person knows whom to talk to), profile id, created_at. Stringer-private fields (internal_notes, nickname, default_tension_memo) are NOT included — those are the stringer's business notes about the client, not the client's personal data. (FADP grants access to the client's data; not to the stringer's notes about the client.) |
orders[] |
Every order across all stringers, full receipt-relevant fields (racket spec, string spec, tensions, pricing, lifecycle dates, comments included verbatim per OQ-F-4 decision 2026-05-04 — symmetric with the receipt-comments-visible decision (OQ-R-1, CC-2026-05-04); the customer already has the data via the receipt, redacting in DSAR would be artificially asymmetric). Each order tagged with the stringer who performed it. |
share_grants[] |
Every grant where this Person is the subject of the data being shared, regardless of who the granter was. For each: rule type (1 / 2 / 3), granter_kind, granter ID (resolved to a display name), grantee stringer ID (resolved to a display name), created_at, revoked_at, revoke_reason. |
share_audit_excerpt[] |
Every share_audit row where the row pertains to data about this Person (any access of a job whose ClientProfile resolves to this Person). For each: timestamp, actor (resolved to a display name), event kind, target row reference. This is the "who saw my data and when" log that FADP makes a Person's right. |
notification_log[] (V3+) |
Future placeholder. V2 has no per-Person notification dispatch log to surface; the audit row covers the share-related events. |
processors_at_export_time |
Snapshot of the sub-processor list at the moment of export, so the Person can correlate "which processor might still hold data sent by RBO at any time before this date" if they choose to follow up with each processor independently. |
export_metadata |
exported_at, format_version, request_id (FK back to a row in dsar_log). |
Acceptance criteria¶
- A-DSAR-1. Every Person can request a DSAR. In V2 the request channel is out-of-band (email Stefan); in V3 it is the portal "Download my data" button.
- A-DSAR-2. The response is a single JSON document, in EN or DE per
Person.default_locale(field labels and any human-readable values translated; data values themselves are unchanged). - A-DSAR-3. The response is scoped to the requesting Person only. It MUST NOT include any other Person's data, including data about other Persons sharing a stringer or appearing on the same audit log.
- A-DSAR-4. If the Person has been soft-deleted (
Person.deleted_atset, but not yet finalized), DSAR still works — they retain access to their data during the retention window. - A-DSAR-5. If the Person has been hard-erased (
Person.scrubbed_atset), DSAR returns a minimal stub:{"status": "erased", "scrubbed_at": "<ts>", "request_id": "<id>"}— RBO no longer has PII to disclose. The request is still logged indsar_log(audit invariant). - A-DSAR-6. Every DSAR request and response writes a row to
dsar_log(see Schema asks) — actor, kind (access_request/erasure_request/portability_request), timestamp, status, output reference (e.g. URL or file hash). Append-only. - A-DSAR-7. SLA: 30 days for response, per FADP art. 25 (the platform commits to this; the admin-mediated V2 path requires Stefan to act on the request inside the SLA). Tracked by
dsar_log.requested_atvs.dsar_log.responded_at. - A-DSAR-8. Identity verification before serving a response: in V3 the magic-link flow is sufficient. In V2 (admin-mediated), Stefan MUST confirm the requester's identity by replying to the verified email on the
Personrow, or via another channel of equivalent confidence; thedsar_logrow notes the verification method.
Right to erasure ("right to be forgotten")¶
A Person is entitled to request that RBO delete the data it holds about them.
Surface¶
- V2 — admin-mediated, same channel as DSAR.
- V3 — self-service via the portal.
Two-step erasure (soft-delete then hard-erase)¶
Erasure is two-stage to allow mistake-recovery (mirrors the stringer-side 90-day grace from the stringer lifecycle, but with a shorter window since the Person did not authenticate twice — they made one request and could be reversing within days):
identified Person ─► soft-deleted ─► (30-day grace) ─► hard-erased
▲ │
└──── reverse-on-request ◄─────────┘
(only during grace)
| State | Description | Effect on visibility |
|---|---|---|
| identified | Normal Person row. | Person's data is queryable to the Person via DSAR; visible to stringers via their ClientProfiles + share grants. |
| soft-deleted | Person.deleted_at set. |
The Person can no longer log into V3. Stringers see the ClientProfile but the underlying Person record is flagged "deleted by request"; new orders cannot reference this Person. Existing orders / audit rows are untouched. |
| hard-erased (scrubbed) | Person.scrubbed_at set; PII fields wiped to placeholder values; email cleared (so a future Person with the same email is not silently merged into this row). |
The Person row is not deleted — that would orphan FKs into orders, share grants, and audit. PII is scrubbed; the row remains as a "tombstone" for FK integrity. |
Cascade rules — what happens to data on erasure¶
The erasure decision is the Person's, not the stringer's. The cascade is PII-scrub with retention, not bulk-delete — orders and audit are retained under legal-obligation and legitimate-interest bases.
| Owned entity | Behaviour on hard-erase |
|---|---|
Person row |
PII fields scrubbed: display_first_name, display_last_name set to [redacted]; email cleared; email_verified_at cleared; default_locale retained (no PII); notification_prefs cleared; claim_token cleared. Person.scrubbed_at set. The row is not deleted. |
ClientProfile rows referencing this Person |
The link to Person survives (FK integrity). Stringer-private fields (nickname, internal_notes, default_tension_memo) are retained verbatim — they are the stringer's business notes about the relationship, the stringer's Order.comments-equivalent at the profile level. They are NOT the Person's personal data; the Person's right to erasure does not reach them. (Compare to the stringer-side scrub on stringer offboarding — that's the stringer's data, scrubbed when the stringer leaves; here it's the stringer's notes, retained when the client leaves.) Stringer notification on the cascade: see Stringer-side notification. |
Order rows referencing this Person via ClientProfile |
Retained (Swiss CO art. 957–958f, 10-year retention floor for receipts as supporting documents to the stringer's books). Order PII surfaces (the receipt-snapshotted client name, address-on-receipt if present) are scrubbed via the same [redacted] placeholder: from the moment of erasure, any re-render or re-emit of the receipt shows the redacted name. Already-emitted receipt PDFs in customer inboxes / printed copies are NOT recallable (same caveat as the share-revocation invariant in client identity & sharing — out of scope). |
Order.comments |
Scrubbed to [redacted by request] placeholder (free text may have client PII the stringer added). |
Order.receipt_emit_log rows |
The audit shell (timestamps, emit kind, recipient stringer, receipt number) is retained verbatim — that is part of the FADP-defensible audit trail and the Swiss CO supporting-document set. The per-emit field snapshot (the receipt-affecting fields at emit time blob added per order lifecycle — old receipt version retention) has its PII fields scrubbed via the same placeholder, while structural / financial / spec fields are retained. |
order_shares rows where the data subject of the shared order is this Person |
The grant is revoked (revoked_at = scrubbed_at, actor_kind = system, meta = {"reason": "subject_erasure"}). One audit row per revocation. |
person_stringer_share rows for this Person |
Same — revoked at scrub time, audit-logged. |
share_audit rows where the actor was this Person, or the target was data about this Person |
Retained verbatim. The audit log is the FADP-defensible record of who-saw-what; redacting it would defeat the very rights it documents. The actor Person ID stays as a reference; the displayable identity is resolved at read time and shows [redacted] once the Person is scrubbed. |
dsar_log rows for this Person |
Retained verbatim (legitimate interest: prove the platform processed prior requests). |
Stringer-side notification on erasure¶
When a Person erases, the stringers who hold ClientProfiles for that Person are notified — the stringer's view of their client suddenly shows [redacted] and they need an explanation. Notification channels follow the client identity & sharing notification rules (in-app mandatory; email opt-in via stringer prefs). Message: "A client has exercised their right to erasure under FADP. Their ClientProfile in your view will show as redacted from now on. Your historical orders and receipts are retained per Swiss commercial-record requirements; their PII is scrubbed."
Acceptance criteria¶
- A-ERA-1. Soft-delete is reversible by Stefan (V2) or by the Person themselves via re-magic-link (V3) within the 30-day grace window. Reversal clears
Person.deleted_at. - A-ERA-2. Hard-erase is only callable after the 30-day grace expires (parallels the stringer-side admin-finalize-after-grace rule). Admin-triggered, not auto-cron — see Schema asks — clean-up job.
- A-ERA-3. Hard-erase runs the cascade rules in a single transaction.
- A-ERA-4. Stringers holding a ClientProfile for the erased Person are notified per Stringer-side notification.
- A-ERA-5. Every erasure request, soft-delete, reversal, and hard-erase writes a
dsar_logrow. - A-ERA-6. A Person cannot be soft-deleted if there is a pending DSAR or portability request for that Person — those must be served first or cancelled by the Person.
- A-ERA-7. A Person scrubbed once is permanently scrubbed:
scrubbed_atis set-once and cannot be cleared. The tombstone row is never re-used.
Right to data portability¶
FADP grants the data subject the right to receive their data in a "structured, commonly used, machine-readable format" and have it transmitted (where feasible) to a system of their choosing.
Surface¶
- V2 — admin-mediated; the same path as DSAR. The portability format equals the DSAR JSON document; FADP makes the two rights largely overlap in practice.
- V3 — the "Download my data" portal button serves both DSAR (read) and portability (download).
Distinction from DSAR and from M20 stringer export¶
| Surface | Subject | Granter | Format |
|---|---|---|---|
| DSAR (this page) | One Person | The Person themselves (V3) or admin (V2) | JSON, scoped to one Person. |
| Portability (this page) | One Person | The Person themselves (V3) or admin (V2) | Same JSON — the FADP rights collapse into one document. |
| M20 per-stringer export (V2 scope, NFR-4) | One Stringer's whole dataset | The Stringer | XLSX (familiar) + JSON (full context). Includes ALL of that stringer's clients across the stringer's entire workspace. |
The two M20 outputs serve different audiences:
- M20 / NFR-4 self-service is for the stringer's continuity — taking their own work product to a future tool, or backing up their workspace.
- DSAR / portability (this page) is for the client's rights — a per-Person dump scoped strictly to data about that one human.
Pax MUST NOT collapse the two pipelines: the DSAR pipeline applies the Person-scope filter at every join; M20 does not.
Acceptance criteria¶
- A-PORT-1. The portability response is identical in shape to the DSAR response (same JSON document; same per-Person scoping invariant).
- A-PORT-2. The format is JSON (UTF-8). XLSX may be added later if Stefan flips OQ-F-2; for V2, JSON only.
- A-PORT-3. Portability requests are tracked in
dsar_logwithkind = portability_request(distinct from access requests, even if the response is identical) so the platform can report on the rights people exercise.
Retention policy¶
| Data class | Retention | Justification |
|---|---|---|
| Order rows + receipt-emit-log structural fields | 10 years from strung_at (the receipt-final moment per order lifecycle). |
Swiss Code of Obligations art. 957–958f — bookkeeping records and supporting documents must be retained 10 years. Receipts are supporting documents for the stringer's books, even though the platform itself is hobby-scale. Aligning to the legal floor lets stringers who later go formal-business not face data-loss. |
Order PII surfaces (client name on receipt snapshot, Order.comments) |
Until erasure request (FADP art. 6, deletion-on-request). On erasure: scrubbed to placeholder; structural / financial / spec fields retained for the 10-year window above. | FADP overrides the CO retention for personal data content; the CO requires the document, not the unredacted name on it. A redacted receipt is a valid supporting document under CO. |
share_audit rows |
5 years from event timestamp. | Legitimate interest (FADP art. 31 par. 2 lett. c): demonstrate compliance; investigate disputes about who-saw-what. 5 years balances "keep enough to defend a complaint" against "don't keep more than necessary". Open question OQ-F-1 below. |
dsar_log rows |
5 years from responded_at. |
Mirrors share_audit; demonstrates the platform served data-subject rights properly. |
Soft-deleted Person |
30 days from deleted_at, then hard-erase eligible (admin-triggered). |
Mistake-recovery window. Shorter than the stringer-side 90-day grace because (a) the Person did not authenticate to deactivate (they made one request that may have been a mistake), (b) the cost of accidental erasure is recoverable for the stringer (the ClientProfile is retained — the stringer can re-add the Person if they re-onboard) but irrecoverable for the Person's own portal access (their email link breaks). 30 days mirrors the cancellation window in common SaaS practice. Open question OQ-F-2 below. |
Soft-deleted Stringer (deactivated_at set, before finalized_at) |
90 days per stringer lifecycle. | Already specified there; restated for completeness. |
| Stringer business-identity fields after admin-finalize | Scrubbed at finalized_at; row retained for FK integrity. |
Stringer lifecycle is authoritative. |
Order.receipt_emit_log per-emit snapshots |
Same as the parent Order — 10 years; PII scrubbed on Person erasure (see cascade rules). | Same justification as Order rows. |
Backups (platform pg_dump to Hetzner Storage Box, NFR-4) |
90-day rolling window, then dropped. | Backups are operational, not archival. Erasure cannot reach into already-rotated-off backups; the 90-day cap caps the FADP exposure. Important caveat in the privacy notice — see Privacy notice skeleton — required content. |
| Resend mail logs (sub-processor) | Per Resend's own retention; outside RBO control. | RBO does not control this; the privacy notice discloses Resend as a sub-processor and links to their policy. |
Acceptance criteria¶
- A-RET-1. Retention windows are configuration constants the admin can review (not hard-coded magic numbers in random places). Pax flag.
- A-RET-2. A clean-up job exists per Schema asks — clean-up job — admin-triggered, not cron — that finalizes soft-deleted Persons whose 30-day grace has expired.
- A-RET-3. Backups are documented as a 90-day window in the privacy notice; erasure is best-effort with respect to backups (FADP recognises this as a reasonable operational cap).
Consent records¶
Consent surfaces in the model today¶
- Stringer onboarding. When admin invites a stringer and the stringer accepts via magic-link, the click constitutes consent to the platform's terms (which the privacy notice will reference). The acceptance is captured implicitly by the existence of the
Stringerrow +email_verified_at. - Person magic-link claim. When a Person claims their
Personrow via the V3 portal magic-link, the click constitutes consent to platform participation as a Person (read history, exercise rights, optionally grant Rule 2 / Rule 3 shares). Captured byemail_verified_at. - Share grants under Rule 2 / Rule 3 (client identity & sharing, ADR-0004). These are explicit, typed consent by the Person to share with a stringer. The
granter_kindfield on every grant is the consent record: whengranter_kind = person, the Person is the consenting party; whengranter_kind = stringer, the stringer is acting on legitimate-interest (their own work product — Rule 1) without Person consent. - Person notification preferences (
Person.notification_prefs). Per CC-2026-05-02-2: in-app channel is mandatory baseline (no consent required — operational); email is opt-in (consent recorded as the preference value).
Stringer-created draft Persons — consent-by-proxy and ratification¶
The load-bearing case: a stringer creates a Person row on behalf of a not-yet-onboarded client (see client identity & sharing — pre-login clients). At creation time there is no consent from the Person — they have not interacted with the platform.
Legal basis at creation: legitimate interest (FADP art. 31 par. 2 lett. d — necessary for the performance of a contract to which the data subject would foreseeably be a party). The stringer is creating a record to perform the stringing service the Person ordered; the platform is the venue for that record. The data minimisation principle is satisfied because the draft Person is created with only the fields the stringer needs (display name, optional email, optional locale).
Ratification flow. When the draft Person is later sent a magic-link (V3 portal onboarding) and the recipient verifies the email, the verification click ratifies the prior consent-by-proxy creation. Operationally:
- The magic-link landing page (V3) shows a one-screen "RBO already had a record for you, created by Stringer X on
. By continuing, you confirm this record and accept our privacy notice." consent screen. - The Person can decline at this screen — the system then offers them the right to erasure (the soft-delete path above) without ever fully on-boarding them.
- On accept,
email_verified_atis set; the prior implicit consent-by-proxy is upgraded to explicit consent. No retroactive permission expansion happens — the stringer's existing Rule-1 grants continue to follow Rule-1 redaction; the Person now has the agency to issue Rule-2 / Rule-3 grants going forward.
Acceptance criteria¶
- A-CONS-1. Every Person creation event records who created it (stringer ID for draft Persons;
null/systemfor self-creation via V3 portal post-magic-link). Pax flag (likely acreated_by_kind+created_by_idpair onPerson, or surfacing viashare_audit). - A-CONS-2. The V3 magic-link claim landing page surfaces the consent-ratification screen (see Hidden requirements — Mira's lane).
- A-CONS-3. Decline-on-claim path triggers the soft-delete erasure flow (the Person never gets a chance to be merged into a verified state; instead they exit gracefully).
- A-CONS-4.
Person.notification_prefsis the canonical store for opt-in choices (per CC-2026-05-02-2). Changes write a row inshare_auditwithevent_kind = consent_changeso the consent history is reconstructable.
Processor / sub-processor disclosure¶
RBO uses the following third parties to process personal data. They are listed in the public privacy notice; changes to this list trigger user notification per Open question OQ-F-3.
| Processor | Role | Personal data processed | Region | Reference |
|---|---|---|---|---|
| Resend | Transactional email — receipts to clients (V2), magic-links + notifications (V3). | Recipient email; sender display name; receipt PDF body (contains client display name + order details); subject line. | EU (Resend EU-region SMTP endpoint, where supported). | keystone ADR-0005 |
| Keystone-managed Postgres (Hetzner kst1) | Application data storage. | Everything in the schema — Persons, Stringers, ClientProfiles, Orders, share grants, audit. | Hetzner Helsinki (FI) / Falkenstein (DE) per keystone hosting. | NFR-1 (resolved out-of-band, platform proposal). |
| Hetzner Storage Box | Encrypted nightly pg_dump backups. |
Same as Postgres above, in dump form, 90-day rolling. | Hetzner FI / DE. | NFR-4. |
| GitLab (Pages + CI) | Public docs site (this docs site) + CI runners. | None — docs are public; CI does not process personal data. Listed for completeness; not a personal-data processor under FADP. | Per GitLab.com hosting. | mkdocs.yml (project root). |
| gotrue (keystone-managed Supabase Auth) | Authentication: stringer onboarding magic-links (V2), Person magic-link claims (V3). | Email; auth tokens; login timestamps. | Same hosting as keystone Postgres (kst1). | docs/architecture/auth-and-tenancy.md. |
RBO is not a sub-processor of any other system in production; it is a leaf application. (The keystone platform is a sibling, not a processor — RBO uses keystone-provided runtime + DB + auth, but there is no upstream client whose data RBO processes on behalf of someone else.)
Acceptance criteria¶
- A-SUB-1. The privacy notice (see skeleton) lists every entry in the table above and links to each processor's own privacy policy.
- A-SUB-2. Adding or removing a processor MUST trigger a privacy-notice update before routing data to the new processor; the change is logged in the project change log (CC entry) so the audit trail records when the change was made.
- A-SUB-3. Per OQ-F-3, processor list changes do not require user re-consent in V2 (notify-only is the proposed default); revisit if Stefan flips.
Privacy notice skeleton¶
The public-facing privacy notice (linked from the V2 stringer dashboard footer + V3 client-portal footer) MUST contain at minimum the following sections. The actual text is a legal-review deliverable, not Iris's; this page only specifies the skeleton.
- Data controller identity. Per stringer, not platform-level — each stringer is the controller for the data of their own clients (the platform-as-multi-tenant is the processor on their behalf). The notice MUST display the stringer's
business_name(ordisplay_nameifbusiness_nameis null),business_address, andemailper the stringer's profile (see stringer-lifecycle — onboarding fields). For the platform-level role (e.g. Persons interacting via the V3 portal pre-stringer), the platform itself (Stefan's role as platform operator) is the controller. - Processing purposes. Receipt issuance, stringing-job tracking, optional cross-stringer sharing per client identity & sharing, client-portal access (V3), audit / compliance.
- Lawful bases. Cross-reference Scope of FADP applicability — contract performance, legal obligation (CO retention), legitimate interest (audit), consent (Person notification opt-ins, Rule-2 / Rule-3 grants).
- Categories of personal data. Stringer business-identity fields, Person display-name + (optional) email, order data (racket, strings, tensions, pricing, lifecycle dates, comments), audit log entries.
- Sub-processors. Reference the processor table; link out to each processor's privacy policy.
- Retention. Reference the retention table; call out the 10-year CO floor, the 30-day Person-erasure grace, the 90-day backup window.
- Data-subject rights. Right to access (DSAR), right to erasure, right to data portability, right to object, right to rectification (handled inline via the Person's profile + ClientProfile rectification by stringers — operational, not a separate flow). Channel for exercising rights: V2 = email Stefan; V3 = portal self-service. Reference the SLA — A-DSAR-7 (30 days).
- International transfers. None outside Switzerland / EU / EEA in the current processor list (Hetzner FI / DE; Resend EU). The notice states this and commits to disclosing future non-EU/EEA processors.
- Contact for privacy queries. Stefan's email; response SLA 30 days.
- Notice update mechanism. Reference the project change log (CC entries on this page); commit to surfacing material changes via in-app notification + email per OQ-F-3.
- Date of last update. Auto-stamped from the docs build.
Acceptance criteria¶
- A-NOTICE-1. A
docs/legal/privacy-notice.md(or equivalent path — Stefan/legal-review's choice) exists and contains all 11 sections above before the first non-Stefan stringer is onboarded. This page does not write that file — that's a legal-review deliverable; it is filed as a follow-up. - A-NOTICE-2. The stringer dashboard footer + V3 portal footer link to the privacy notice on every page.
- A-NOTICE-3. Material updates to the notice (e.g. new sub-processor) trigger an in-app notification to all active stringers + Persons, per OQ-F-3.
Schema asks for Pax / Theo¶
The FADP rights surface above requires schema support beyond what Phase-1 currently has. These are requirements, not the schema itself — Theo owns the data-model amendment, Pax owns the Phase-1.x slice that lands them. Filed as follow-up issues to the racket-book#98 issue this MR closes.
Formalised on 2026-05-05 as numbered requirements R-FADP-1 … R-FADP-7 in fadp-implementation-asks.md. That page is the canonical target for Theo's data-model amendment + Pax's Phase 3.x slice; the list below is the original ask + a one-line forward reference to each. Tracked in racket-book#111.
Person.deleted_at(timestamptz, nullable). Soft-delete marker. Set at erasure-request time; cleared on within-grace reversal. Already mentioned in ADR-0004 §FADP positioning and on stringer-lifecycle — FADP positioning; verify it is in the current data-model and add it if not. Theo: please confirm presence indocs/architecture/data-model.mdor amend.Person.scrubbed_at(timestamptz, nullable, set-once). Hard-erase tombstone marker. Cannot be cleared once set. Adding this is new; flag for Theo to amend ADR-0004's data-model + the architecture data-model page.dsar_logtable. Append-only log of every DSAR / erasure / portability request and response. Suggested columns:id,person_id(FK, retained even after scrub),kind(enum:access_request/erasure_request/portability_request),requested_at,responded_at,status(enum:pending/responded/cancelled),requested_by_kind(person/admin),response_ref(URL or file hash; nullable),verification_method(free text — V2 admin notes, V3 magic-link auto-fills),meta(JSONB for free-form notes). RLS-equivalent: admin-only read in V2; the requesting Person sees their own rows in V3. Flag for Theo to write the schema; Pax to land in the slice that adds the DSAR endpoints.Person.created_by_kind+Person.created_by_id(or surface via ashare_auditevent withevent_kind = person_created). Whichever Theo prefers; the requirement is that the consent-by-proxy provenance of every draft Person is reconstructable. (Per A-CONS-1.)- Admin-triggered finalize-soft-deleted-persons job. Specification (not implementation): admin endpoint
POST /admin/persons/finalize-expiredthat scansPersonrows withdeleted_at IS NOT NULL AND deleted_at < NOW() - INTERVAL '30 days' AND scrubbed_at IS NULL, runs the erasure cascade for each in a transaction, and returns the count finalized. Not cron — the human-in-the-loop principle from B8 of the stringer-lifecycle applies here too. Pax flag. - Order PII scrub helper. Function
scrub_orders_for_person(person_id)invoked in the erasure cascade; replaces PII surfaces inOrder.commentsand the per-emit snapshots' name-fields with the[redacted]placeholder. Pax to design; Theo to bless the call sites. share_audit.event_kindenum extension. Addperson_erasure,dsar_served,consent_changeevent kinds. Theo flag (ADR-0004 amendment).
Hidden requirements (UI affordances the rules imply)¶
UX surfacing is owned by Mira — flagged as follow-up requirements; not in scope for this requirements doc.
- Admin "Process DSAR" workflow in the V2 admin view: a queue of
dsar_logrows inpendingstatus, a "generate response" button per row that runs the export pipeline, a "mark responded" button that uploads or links the JSON response. - V3 portal "Download my data" affordance — exposes DSAR + portability via a single button (the response is identical).
- V3 portal "Delete my account" affordance — soft-delete trigger with a clear "you have 30 days to reverse this" message, recovery magic-link path.
- V3 magic-link claim landing — consent-ratification screen — see Stringer-created draft Persons for content; Mira designs the surface.
- Privacy-notice link in every footer — V2 stringer dashboard + V3 client portal.
- Stringer notification UI when a client of theirs has erased — message + redacted-display effect on the ClientProfile view.
- Admin "Finalize expired soft-deletes" affordance — list of soft-deleted Persons whose 30-day grace has expired; bulk-finalize action with typed-confirm pattern (per the stringer-lifecycle finalize convention).
Open questions¶
- OQ-F-1 (Stefan to confirm or flip): retention window for
share_auditanddsar_logrows. - Default applied: 5 years from event timestamp /
responded_at. Rationale: long enough to defend an FADP complaint; short enough to satisfy data-minimisation. Could be 10 years to align with CO; could be 3 years to lean privacy. 5 years is the proposed middle. - One-flag flip if Stefan prefers 10 years (alignment with CO floor) or 3 years (privacy-leaning).
- OQ-F-2 (Stefan to confirm or flip): retention window for soft-deleted
Personbefore hard-erase becomes admin-eligible. - Default applied: 30 days. Rationale: mirrors the stringer-lifecycle 90-day grace pattern but with a shorter clock — the Person did not authenticate to erase (they made one request that may have been a mistake reachable by mistake-recovery), and the cost of accidental erasure to the Person is low (the stringer can re-add them). 30 days matches the cancellation window common in SaaS.
- One-flag flip if Stefan prefers 90 days (consistency with stringer side) or 7 days (faster response to user intent).
- OQ-F-3 (Stefan to confirm or flip): behaviour on processor / sub-processor list changes.
- Default applied: in-app notification + email (where opted in) per CC-2026-05-02-2 channels; no re-consent gate. Rationale: common SaaS practice; FADP requires transparency, not re-consent, on processor changes. Re-consent would create a heavyweight pop-up gate that hurts UX without measurable privacy gain (a user who declines can't proceed; a user who accepts is just clicking through).
- One-flag flip if Stefan prefers a re-consent gate (modal + accept-required to continue using the platform after a sub-processor change).
- OQ-F-4 — closed 2026-05-04: DSAR response surfacing of
Order.comments. - Decision: visible in DSAR response. Rationale: comments are visible to the client on the receipt per CC-2026-05-04 (OQ-R-1); the customer already holds the data. Redacting in DSAR while exposing on receipt would be artificially asymmetric — one source of truth, one visibility posture. Operational consequence: stringers should treat
commentsas customer-facing in all contexts. If a future need surfaces for stringer-private working notes, that becomes a new field (e.g.ClientProfile.internal_notesalready exists for stringer-scoped private memos), not a hidden surface onOrder.comments. - OQ-F-5 (Stefan to confirm or flip): can a Person who hard-erased and was scrubbed later re-onboard with the same email?
- Default applied: yes, treated as a new Person + new gotrue user ID (mirrors OQ-S-3 from stringer-lifecycle). The scrubbed tombstone row keeps
email = NULLprecisely so a future Person with the same email is not silently merged. - One-flag flip if Stefan prefers blocking re-onboard at the previously-erased email (would require a
scrubbed_emailsdeny-list or a non-NULL retainedemail_hashon the tombstone — a small schema cost for a corner-case policy).
Cross-references¶
- Identity model: client identity & sharing — Person/ClientProfile split + three sharing rules. The audit + grant log this page builds on.
- Stringer side: stringer lifecycle — the parallel page for stringer-side scrub on offboarding. The two pages together cover the full FADP cascade for both data-subject classes.
- Architecture / authoritative ADR: ADR-0004 —
granter_kind(consent record),share_audit(FADP-defensible audit log),Person.deleted_at(mentioned in passing; this page surfaces the requirement explicitly). - NFRs: NFR-2 (Resend SMTP, EU region); NFR-4 (backup window — referenced in retention table); NFR-7 (locale rule — DSAR response is localised per
Person.default_locale). - Order lifecycle: order-lifecycle — receipt-emit log retention + per-emit snapshot scrub-on-erasure.
- V2 scope: v2-scope — M20 (per-stringer export) is distinct from DSAR / portability.
- V3 vision: v3-vision — C1 — surfaces self-service DSAR / erasure / portability.
- Coordination: Pax (Phase-1.x schema slice — implements
Person.scrubbed_at,dsar_log, the cascade helper, the admin finalize-expired endpoint); Theo (data-model amendment — Persons, audit event-kind extension); Mira (UX for admin DSAR queue, V3 portal "Download my data" + "Delete my account", consent-ratification screen). - Issue tracking: racket-book#98 (this page).