46 · Personal Data Management

Use the @PersonalData vocabulary, @cap-js/personal-data plugin, right-to-erasure (anonymise), and data portability.

Episode 46 — Personal Data & GDPR Compliance

What you'll learn

Why this matters

GDPR penalties reach 4 % of global revenue. For enterprise apps on SAP BTP, implementing GDPR correctly is a compliance requirement, not optional. CAP's built-in annotations and plugin reduce this from a custom dev project to a configuration task.

GDPR obligations for app developers

Obligation What it means CAP support
Data inventory Know what personal data you store @PersonalData annotations → auto-inventory
Purpose limitation Only store data needed for stated purpose Annotation documentation
Data minimisation Collect minimum necessary fields Annotation + schema review
Right to erasure Delete on request @PersonalData + erasure handler
Right to portability Export in machine-readable format Export action
Breach notification 72-hour reporting obligation Out of scope for CAP

Complete @PersonalData annotation vocabulary

// DATA SUBJECT entity (the person)
annotate Customers with @PersonalData: {
  EntitySemantics : 'DataSubject',
  DataSubjectRole : 'Customer'
};

// RELATED entity (data about the person, but not the person)
annotate Orders with @PersonalData: {
  EntitySemantics  : 'DataSubjectRelated',
  DataSubjectRole  : 'Customer'
};

// Fields on the data subject
annotate Customers with {
  customerNumber @PersonalData.FieldSemantics: 'DataSubjectID';       // unique ID
  firstName      @PersonalData.IsPotentiallyPersonal: true;
  lastName       @PersonalData.IsPotentiallyPersonal: true;
  email          @PersonalData.IsPotentiallyPersonal: true;
  dateOfBirth    @PersonalData.IsPotentiallySensitive: true;           // special category
  healthRecord   @PersonalData.IsPotentiallySensitive: true;           // special category
}

// Field on related entity that links back to the data subject
annotate Orders with {
  customer @PersonalData.FieldSemantics: 'DataSubjectID';   // FK to Customers
}

CAP Personal Data plugin — auto data inventory

npm install @cap-js/personal-data

The plugin:
1. Reads @PersonalData annotations at startup
2. Builds an in-memory data inventory (which entities, which fields, which data subjects)
3. Intercepts CUD operations to generate audit log entries automatically
4. Provides a /admin/personal-data metadata endpoint for the inventory

Right-to-erasure implementation

// srv/admin-service.cds
service AdminService @(requires: 'admin') {
  entity Customers as projection on my.Customers;

  action  eraseCustomer (customerID : UUID) returns String;
}
// srv/admin-service.js
this.on('eraseCustomer', async (req) => {
  const { customerID } = req.data

  // 1. Anonymise personal fields (preferred over hard delete for referential integrity)
  await UPDATE('Customers').set({
    firstName:   '[ERASED]',
    lastName:    '[ERASED]',
    email:       `erased-${customerID}@deleted.invalid`,
    dateOfBirth: null,
    phoneNumber: null,
    erasedAt:    new Date()
  }).where({ ID: customerID })

  // 2. Delete orphaned personal data in related entities
  await DELETE.from('CustomerAddresses').where({ customer_ID: customerID })
  await DELETE.from('CustomerPaymentMethods').where({ customer_ID: customerID })

  // 3. Log the erasure (mandatory for GDPR documentation)
  const audit = await cds.connect.to('audit-log')
  await audit.log('PersonalDataErasure', {
    data_subject: { type: 'Customer', id: customerID },
    user:         req.user.id,
    reason:       'GDPR Art. 17 - Right to Erasure'
  })

  return `Customer ${customerID} data erased`
})

Right-to-portability (data export)

this.on('exportCustomerData', async (req) => {
  const { customerID } = req.data

  const [customer, orders, addresses] = await Promise.all([
    SELECT.one.from('Customers').where({ ID: customerID }),
    SELECT.from('Orders').where({ customer_ID: customerID }),
    SELECT.from('CustomerAddresses').where({ customer_ID: customerID })
  ])

  // Return as JSON (GDPR requires machine-readable format)
  return {
    exportDate:  new Date().toISOString(),
    dataSubject: customerID,
    data: {
      profile:   customer,
      orders:    orders,
      addresses: addresses
    }
  }
})

Data retention policy

entity Orders {
  key ID        : UUID;
  status        : String;
  createdAt     : Timestamp @cds.on.insert: $now;
  retainUntil   : Date;     // set to createdAt + 7 years (legal retention)
  personalDataErasedAt : Timestamp;  // set when customer erased
}
// Scheduled cleanup job (runs daily via SAP Job Scheduling)
module.exports = async (req) => {
  const today = new Date().toISOString().split('T')[0]
  const expired = await SELECT.from('Orders').where(`retainUntil < '${today}'`)

  for (const order of expired) {
    await UPDATE('Orders').set({ buyer_name: '[ERASED]', buyer_email: null })
                          .where({ ID: order.ID })
  }
}

GDPR documentation auto-generated by plugin

GET /admin/personal-data

{
  "entities": [
    {
      "name":    "Customers",
      "role":    "DataSubject",
      "fields":  ["firstName","lastName","email","dateOfBirth"],
      "sensitive": ["dateOfBirth"]
    },
    {
      "name":    "Orders",
      "role":    "DataSubjectRelated",
      "subject": "customer_ID → Customers"
    }
  ]
}

Hands-on exercise

  1. Add @PersonalData annotations to your Customers entity (firstName, lastName, email)
  2. Install @cap-js/personal-data and verify the data inventory endpoint
  3. Implement an eraseCustomer action that anonymises rather than deletes
  4. Implement an exportCustomerData function that returns all data as JSON

Checkpoint ✓

Sign in for progress tracking & Q&A →