Use the @PersonalData vocabulary, @cap-js/personal-data plugin, right-to-erasure (anonymise), and data portability.
@PersonalData annotation vocabularyGDPR 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.
| 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 |
// 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
}
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
// 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`
})
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
}
}
})
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 })
}
}
GET /admin/personal-data
{
"entities": [
{
"name": "Customers",
"role": "DataSubject",
"fields": ["firstName","lastName","email","dateOfBirth"],
"sensitive": ["dateOfBirth"]
},
{
"name": "Orders",
"role": "DataSubjectRelated",
"subject": "customer_ID → Customers"
}
]
}
@PersonalData annotations to your Customers entity (firstName, lastName, email)@cap-js/personal-data and verify the data inventory endpointeraseCustomer action that anonymises rather than deletesexportCustomerData function that returns all data as JSON@PersonalData.EntitySemantics: 'DataSubject' marks the person entity'DataSubjectRelated' with a FK to the data subject