Skip to content

Personal data is minimized at the adapter boundary, before any persistence

Status: accepted Date: 2026-07-10 Deciders: Chris (solo founder) Depends on: ADR-0011 (local hub and ownership boundary) Applies first in: ADR-0005 (CVR deltager ingestion)

Context

Several current and future sources carry personal data about natural persons: CVR participants (deltagere), Ejerfortegnelsen, Tinglysningen (Personbog), Statstidende, Teledata, and eventually scraped people mentions. The prephase ADR-0003 addendum required one uniform decision instead of per-source improvisation.

The first concrete case forces the issue: the deltager type on distribution.virk.dk returns natural persons' real names and residential addresses with nothing more than a Basic Auth credential — while the same data on Datafordeleren (CVR_CVRPerson) is gated behind OAuth + MitID Erhverv. The upstream access-control asymmetry does not make the data less sensitive.

The product needs the people/roles graph (directorships, ownership, board seats — the M:N core of the hub and its highest-value relationship signal). It has no identified need for persons' home addresses or other non-role attributes.

Decision

Personal data is minimized inside the adapter/mapper, before anything is persisted anywhere — including raw snapshots (GCS extracts) and any raw JSONB column. Concretely:

  • Stored for a natural person: the registry surrogate key (enhedsNummer), current name, participant kind (person / company / other), and role/ownership data (role type, function, ownership and voting percentages, validity periods).
  • Dropped at the boundary: residential addresses and every other non-role person attribute. CPR numbers are never stored (this surface does not expose them; the rule still stands for any future surface that does).
  • Key model: registry-sourced people are keyed on enhedsNummer. Externally-sourced people (scraped, commercial) have no such key; they require a separate resolution/alias layer, which remains an open design problem and must not be solved by widening this rule.
  • Posture: where upstream platforms disagree about gating (the ES/Datafordeler asymmetry), we self-impose the stricter platform's posture.
  • Lawful basis: processing of the retained fields rests on legitimate interest in publicly published business-registry data (roles people hold in companies are published by Erhvervsstyrelsen for exactly this transparency purpose). Minimization is what keeps that basis proportionate: we retain only what serves the business-relationship purpose.

This rule is cross-cutting: every future adapter touching people data (Ejerfortegnelse, Tinglysning, Statstidende, Teledata, scraped sources) inherits it and documents any source-specific extension in its own ADR.

Options considered

  • Store full deltager records (rejected). Persisting home addresses of ~1.8M people with no product need is the hardest GDPR posture to defend and contaminates every downstream copy (raw snapshots, backups, GCS).
  • Defer people/roles entirely (rejected). Loses the typed M:N edge table that the hub shape (prephase ADR-0003) and graph strategy (prephase ADR-0006) are built around, for a risk minimization already addresses.
  • Minimize at the adapter boundary (chosen). The sensitive fields never enter any store, so there is nothing to purge, no raw copy to re-redact, and the lawful-interest argument stays proportionate.

Consequences

  • The GCS raw-extract rule from ADR-0005 has one exception by design: the deltager extract is minimized before it is written, so raw-level rebuild for people is a re-fetch, not a re-read.
  • people rows carry no raw JSONB column, unlike companies and production_units — an accepted inconsistency in the storage contract, bounded to person entities.
  • If a future product feature genuinely needs a dropped field, that is a new ADR with its own lawful-basis analysis and a re-backfill from upstream — deliberately expensive, which is the point.
  • Adapter tests must assert the absence of dropped fields in mapper output and in the extract writer, not just the presence of kept ones.

Implementation note (2026-07-19, issue #23)

Implemented in ingesters/cvr/mappers.py's map_deltager / minimize_deltager_for_extract — see ADR-0005's 2026-07-19 people & roles amendment for the delivery details. The minimization is structural (allowlist reconstruction), not a denylist filter, so there's no dropped field to accidentally miss; mapper and pipeline-seam tests assert the absence of residential-address fields per this ADR's testing requirement.

Open questions

  • Entity resolution for externally-sourced people (no enhedsNummer): alias/resolution layer design deferred until a class-B source lands (prephase open question, unchanged).
  • Whether people.name itself warrants pseudonymization in exports/read contracts consumed outside the platform — decide when an external-facing consumer exists.