Skip to main content

Data map and RoPA

A data map says where every piece of personal data lives and where it travels. A RoPA (record of processing activities) says why we process it. Keep both in one register that engineers update in the same pull request as the code. Without it, a deletion request depends on someone's memory.

How it works​

The key idea: the diagram is generated from the register, it is not drawn. If a claim is not a row, it is not on the diagram. A hand-drawn diagram is right for three weeks and then it lies.

A DFD (data flow diagram) shows systems as boxes and data movements as arrows. A RoPA is the record GDPR Article 30 asks controllers and processors to keep: purposes, categories of people and data, recipients, transfers, retention and security measures.

Why field-level​

"The users table holds user data" cannot answer the questions that matter:

  • Which fields are special category data (religion, health, sexual orientation, biometrics)?
  • On an erasure request, what exactly do we delete, anonymise, or lawfully keep?
  • When a new column appears in a migration, is it registered, or is it drift?

So the smallest unit is a data element (one logical field), and every place it exists is a separate location row.

data_element "Email address"
location regional_db.users.email plaintext, primary copy
location analytics.user_properties.email_hash hashed
location auth_provider.user_record plaintext, third party
location cache session:{id}.email plaintext, TTL 24h
location error_tracker event.user.email should be scrubbed: verify

Classification lives on the element. How to delete it lives on the location.

How to build it​

Fields per entry​

system (a box on the diagram)

FieldNotes
key, name, kindapp, service, datastore, queue, job, third_party
environment, regionWhere it runs. Region matters for transfers
owner_personRequired. Someone answers for it
vendor_idSet for third parties

data_element (one logical field)

FieldNotes
key, display_nameuser.email, profile.religion
categoryidentity, contact, device, location, behavioural, content, financial, special category
is_special_categoryGDPR Article 9 data needs an Article 9 condition, usually explicit consent
is_child_dataDrives children's rules
is_free_textFree text can contain anything, including special category data
sourceFrom the person, derived, inferred or from a third party. Changes the notice duty
hash_before_egressMatches the analytics schema's PII flag
dsr_access, dsr_erasure, dsr_portabilityWhat a request does to this field
erasure_exemption_reasonRequired when we keep it after an erasure request

data_element_location (one physical copy)

FieldNotes
system_id, locatorregional_db.users.email, s3://example-bucket/avatars/{user_id}
formplaintext, hashed, encrypted, tokenised, derived
is_primary_copyThe system of record
erasure_mechanismConcrete: the SQL, API call, job name or runbook link
retention_rule_idOverrides the element default
evidence_ref, last_verified_atThe file or migration that proves it, and when someone checked

processing_activity (one RoPA row)

FieldNotes
name, purposePlain words, one purpose per row
controller_entity, our_roleWhich legal entity, and controller or processor
gdpr_lawful_basisconsent, contract, legal obligation, vital interests, public task, legitimate interests
lia_refRequired when the basis is legitimate interests
dpdp_basisconsent or a listed legitimate use. DPDP has no legitimate interests
consent_purpose_codeLink to the consent registry
elements, recipients, retention, security_measuresArticle 30 content
profiling, automated_decision, large_scale_special_categoryAny true means a DPIA is likely needed
dpia_status, review_due_at, owner_person

flow (an arrow on the diagram)

FieldNotes
source, target, mechanismHTTPS, websocket, queue, SDK, batch job, manual export
auth_requirementpublic is the value to watch
crosses_region, leaves_countryThen transfer_basis is required
elementsWhich fields travel on this edge, and in what form

vendor (a processor)

FieldNotes
roleprocessor, sub-processor, joint controller, independent controller
dpa_status, dpa_urlSigned data processing agreement
transfer_mechanismadequacy, SCCs, other, or missing
trains_on_our_dataCritical for AI vendors
review_due_at

retention_rule

FieldNotes
triggerfrom collection, last activity, account deletion, consent withdrawal
duration_daysNull means kept forever and needs a written reason
actionhard delete, anonymise, archive, review
enforcementautomated, manual, or none
enforcement_refThe job that actually does it

enforcement = none is the most useful field in the whole register. It is the gap list.

The gap dashboard​

Rows that are wrong by definition:

  • A special category element with no Article 9 condition on its activity.
  • Legitimate interests with no assessment on file.
  • DPIA required and not complete.
  • A retention rule with enforcement = none, or kept forever without a reason.
  • A flow that leaves the country with no transfer basis.
  • A vendor with no signed DPA.
  • Data kept after erasure with no exemption reason.
  • Anything not verified within its review cycle.
  • Columns, routes or SDKs found by the scan that are not registered.

Keep it alive​

Run a scheduled scan that compares the code with the register:

SourceCompared with
Database schema and migrationslocations of kind column
Route and GraphQL inventoryflows
Analytics event schema (PII flags)analytics elements
Infrastructure code (buckets, tables, queues)datastores
Package manifests and SDK importsvendors

A new column not in the register shows as unregistered. A registered column that vanished shows as drifted. Both land on the gap dashboard as work.

The register describes where all personal data lives, so it is sensitive itself. Give it its own role in the admin panel and an audit log. It holds no subject data: requests store a hash of the identifier, never the raw value.

Rules​

Rule

A pull request that adds a personal-data field, a new store, a new vendor or a new flow updates the register in the same pull request. Otherwise the next deletion request misses it.

Rule

erasure_mechanism must be concrete. "Handled by the user module" is not a mechanism. Name the job, the SQL or the API call, or write none so it shows as a gap.

Mistakes we made​

  • "Regional personal data, global content" was not true. The data map said user-owned data lived in the home region and the global database held only app content. An audit found community posts, private group chats, moderation reports with free-text allegations and staff accounts on the global tier, by design. A raw device hardware ID was stored globally with no erasure path. A code comment listing "three schemas" was why nobody noticed. Map from the schema, not from the architecture slide.
  • A comment promised a backstop that did not exist. Two files said an S3 lifecycle rule deleted uploaded lead files after a few days. No such rule existed in any environment, so every file was kept forever. Register the rule with its enforcement_ref, and verify it against live infrastructure.
  • Undisclosed processors. The policy named one AI provider. The code used a different one, plus two moderation providers nobody had listed. A vendor list built from package manifests would have caught it.

Checklist​

  • Every new field has a data element with category, special-category flag and DSR stance.
  • Every copy (DB, cache, logs, analytics, vendor) has a location row with a concrete erasure mechanism.
  • Every new vendor has a DPA status and a transfer mechanism.
  • Every flow that leaves the country has a transfer basis.
  • Every retention rule names the job that enforces it.
  • The drift scan is green, or the new gap is ticketed.