UAE PDPL Compliance in R Shiny Applications
Practical Data Handling Patterns for Institutional Platforms
Engineering · Security # UAE PDPL Compliance in R Shiny Applications
Practical patterns for building UAE Personal Data Protection Law compliant Shiny applications — from data model design through to data subject rights implementation.
14 min read Engineering Security PDPL
Why PDPL Matters for HEI Compliance Platforms
UAE Federal Decree-Law No. 45/2021 on Personal Data Protection (the UAE PDPL) applies to any organisation that processes personal data in the UAE — including universities and the technology platforms they deploy. For higher education compliance platforms, this is significant: OBF platforms process student data (enrolment, academic performance), staff data (faculty credentials, workload), and operational data that often includes personally identifiable information.
Building PDPL compliance in as an afterthought — after the platform is live — is the most expensive way to achieve it. Building it in from the data model design phase costs far less and produces better outcomes. This article covers the key patterns BRASS Digital Lab uses on every platform build.
1. Data Minimisation at the Schema Level
The PDPL principle of data minimisation requires that you collect and retain only the personal data necessary for the stated purpose. In a Shiny/SQLite context, this starts with the database schema.
Pattern: purpose-tagged columns
For every personal data column in the schema, document the purpose explicitly in your schema design:
-- ✅ Well-documented, purpose-justified column
CREATE TABLE kpi_submissions (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id), -- Required for audit trail
submitted_at TEXT NOT NULL, -- Required for compliance timestamping
-- NOT storing: user email, name, department -- these are in users table, not duplicated
...
);Avoid denormalising personal data into compliance record tables — link to the users table via foreign key and pull personal data only when generating reports.
Pattern: defined retention fields
Add retention_expires_at columns to tables containing personal data. A scheduled cleanup job (or manual admin action) can then identify and handle records past their retention period:
# In your Shiny server — data retention audit
expired_records <- dbGetQuery(pool, "
SELECT id, created_at, retention_expires_at
FROM user_activity_log
WHERE retention_expires_at < date('now')
")2. Lawful Basis Documentation
The PDPL requires a lawful basis for every category of personal data processing. For institutional compliance platforms, the typical bases are:
| Processing Activity | Typical Lawful Basis |
|---|---|
| KPI data entry by authorised staff | Performance of contract (employment) |
| Student academic data in OBF dashboards | Legitimate interests (regulatory compliance obligation) |
| Audit trail of user actions | Legal obligation (regulatory requirement) |
| Report generation with personal data | Legitimate interests or legal obligation |
| MFA token storage | Security (protection of data subjects) |
Document the lawful basis for each data category in your platform’s Privacy Impact Assessment (PIA) — which should be completed before development begins.
3. Access Control as a PDPL Control
Role-Based Access Control (RBAC) is not just a security feature in a PDPL context — it is a data protection control. It enforces data minimisation by ensuring users only access personal data relevant to their role.
Pattern: server-side enforcement
Critically, RBAC must be enforced at the server level, not just by hiding UI elements. A user with database access but no UI permission should still be blocked:
# ✅ Correct — server-side check on every data query
get_student_data <- function(user_role, programme_id) {
if (!user_role %in% c("admin", "programme_lead")) {
stop("Access denied: insufficient role for student-level data")
}
dbGetQuery(pool,
"SELECT * FROM student_performance WHERE programme_id = ?",
params = list(programme_id)
)
}
# ❌ Wrong — UI-only "hiding" is not an access control
output$student_tab <- renderUI({
if (user_role() == "admin") tabPanel("Students", ...) # still accessible via URL
})Pattern: data scoping by role
Admin users see all data; programme leads see their programme; faculty see their courses. Implement this at the SQL level, not in R filtering after a full data pull:
# ✅ Scoped query — only pulls authorised data
get_kpi_data <- function(pool, user_id, user_role) {
if (user_role == "admin") {
dbGetQuery(pool, "SELECT * FROM kpi_submissions")
} else if (user_role == "programme_lead") {
dbGetQuery(pool,
"SELECT ks.* FROM kpi_submissions ks
JOIN programme_assignments pa ON pa.programme_id = ks.programme_id
WHERE pa.user_id = ?",
params = list(user_id)
)
}
}4. Audit Trail as PDPL Evidence
The PDPL requires that data controllers demonstrate compliance — which means your audit trail is not just operational logging; it is a regulatory evidence record. Every significant data processing action should be logged.
Pattern: standardised audit log table
CREATE TABLE audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
event_time TEXT NOT NULL DEFAULT (datetime('now')),
user_id INTEGER REFERENCES users(id),
action TEXT NOT NULL, -- 'INSERT', 'UPDATE', 'DELETE', 'EXPORT', 'LOGIN'
table_name TEXT, -- affected table
record_id INTEGER, -- affected record
old_value TEXT, -- JSON of previous values (for UPDATE/DELETE)
new_value TEXT, -- JSON of new values (for INSERT/UPDATE)
ip_source TEXT, -- not mandatory but useful for security reviews
session_id TEXT
);Pattern: automatic audit logging via a wrapper function
Rather than manually logging in every server function, use a wrapper:
db_write <- function(pool, sql, params, user_id, session_id, action_desc) {
# Execute the main query
dbExecute(pool, sql, params = params)
# Log the action
dbExecute(pool,
"INSERT INTO audit_log (user_id, action, session_id, new_value)
VALUES (?, ?, ?, ?)",
params = list(user_id, action_desc, session_id,
jsonlite::toJSON(params, auto_unbox = TRUE))
)
}5. Data Subject Rights Implementation
The PDPL grants data subjects rights including access, correction, erasure, and portability. In a Shiny platform context, this means your admin module needs to support these requests operationally.
Minimum admin capabilities for PDPL rights:
- Right of access: Admin can query all data associated with a specific user ID and export to PDF/CSV
- Right to correction: Admin can update personal data fields with a logged reason
- Right to erasure: Admin can anonymise or delete personal data records (while preserving audit trail integrity — replace personal data with
[REDACTED]rather than deleting audit rows) - Right to portability: Admin can export all data for a specific user in a structured format
Pattern: non-destructive erasure
Deleting audit trail records to fulfil an erasure request conflicts with your regulatory logging obligations. The correct approach is anonymisation:
anonymise_user <- function(pool, user_id, admin_id) {
# Anonymise personal data in the users table
dbExecute(pool,
"UPDATE users SET
name = '[REDACTED]',
email = CONCAT('redacted_', id, '@deleted.invalid'),
phone = NULL
WHERE id = ?",
params = list(user_id)
)
# Log the anonymisation action (do NOT delete audit rows)
dbExecute(pool,
"INSERT INTO audit_log (user_id, action, record_id, new_value)
VALUES (?, 'GDPR_ERASURE', ?, 'User data anonymised per PDPL erasure request')",
params = list(admin_id, user_id)
)
}6. MFA as a PDPL Security Requirement
The PDPL requires appropriate technical security measures to protect personal data. For a platform handling institutional and student data, multi-factor authentication satisfies the PDPL’s technical security requirement — and BRASS Digital Lab implements TOTP-based MFA on all accounts as standard.
The TOTP secret itself must be stored in hashed form, not plaintext, in the database:
# Verify TOTP token (using the otp package)
verify_totp <- function(pool, user_id, submitted_token) {
user_secret_hash <- dbGetQuery(pool,
"SELECT totp_secret_hash FROM users WHERE id = ?",
params = list(user_id)
)$totp_secret_hash
# Decrypt secret for verification only (store encrypted, decrypt on use)
secret <- decrypt_secret(user_secret_hash)
otp::totp_verify(secret, submitted_token, window = 1L)
}Summary: PDPL Compliance Checklist for Shiny Platforms
At Design Phase - [ ] Privacy Impact Assessment completed - [ ] Lawful basis documented per data category - [ ] Data minimisation principles applied to schema design - [ ] Retention periods defined per data category
At Build Phase - [ ] RBAC enforced server-side (not just UI hiding) - [ ] Parameterised queries throughout (no SQL injection risk) - [ ] Audit log table and wrapper function implemented - [ ] MFA implemented with hashed TOTP secret storage
At Deployment - [ ] Environment variables for all credentials (no hardcoded secrets) - [ ] HTTPS enforced (shinyapps.io enforces this by default) - [ ] Admin module includes data subject rights workflows - [ ] Privacy notice accessible to all platform users
Ongoing - [ ] Audit log reviewed periodically - [ ] Retention periods enforced (expired data handled) - [ ] Data subject requests responded to within 30 days - [ ] Platform updated when PDPL implementing regulations change