PDP compliance assessment — AHU AI platform
Date: 2026-08-13 · Law: UU No. 27/2022 (Pelindungan Data Pribadi), transition ended 17 Oct 2024 — fully binding today · Scope: OCR engine, chatbot, gateway, observatory audit store, as deployed on ai-ahu staging
Not legal advice. Article mappings are my reading and need counsel sign-off. Every technical finding below is evidence-backed and reproducible.
Bottom line
The platform is architecturally well-placed on the hardest PDP requirement — everything is on-prem, so there is no cross-border transfer problem, which is the issue that sinks most Indonesian AI deployments. What is missing is not infrastructure but the ability to delete, and the paperwork that proves any of it.
One finding is structural and should be treated as blocking before production:
The immutable audit store contains personal data it is physically unable to delete.
| # | Finding | Article | Severity |
|---|---|---|---|
| 1 | Personal data in an append-only store that blocks DELETE | Art 8, 9, 43, 44 | Blocking |
| 2 | retention_days is a dead config knob — nothing enforces it |
Art 43(2) | High |
| 3 | Specific (sensitive) personal data processed with no DPIA | Art 34 | High |
| 4 | No governance artifacts at all (RoPA, notice, DPO, breach runbook) | Art 31, 46, 53 | High |
| 5 | 177 real KTP records in a staging environment with a dev-login path | Art 16(1), 35 | Medium |
What we actually process
Measured from staging (ExtractedField, 6,329 rows):
| Field | Rows | PDP class |
|---|---|---|
nik |
177 | personal identifier (also restricted by UU 24/2013 Adminduk) |
golongan_darah |
177 | specific — health data, Art 4(2)(a) |
npwp |
137 | specific — personal financial data, Art 4(2)(f) |
agama |
177 | arguably specific via Art 4(2)(g) |
tanggal_lahir, tempat_lahir, jenis_kelamin, status_perkawinan, pekerjaan, kewarganegaraan |
177 each | general personal data |
alamat.* (7 sub-fields) |
177 each | general personal data |
Processing specific personal data is the trigger that makes a DPIA mandatory under Art 34 and materially raises the consent/lawful-basis bar. Blood type arrives incidentally — it is on the KTP and we extract it because it is there, not because any flow needs it. That is the cheapest finding to fix on this whole list: stop extracting it.
Finding 1 — the audit store cannot forget (blocking)
ai_call_bodies is append-only and hash-chained: internal/store/bodies_test.go asserts that both UPDATE and DELETE fail. That immutability is a deliberate, valuable control for an audit trail. The problem is what is inside it.
Proven, not assumed. I decompressed the 5 newest engine=ahu-ocr, operation=chat request bodies (content never printed, sample securely deleted):
| Decompressed size of 5 bodies | 10.8 MB |
| 16-digit sequences (NIK shape) | 38 |
Occurrences of nik |
283 |
Person markers (Tuan/Nyonya/lahir di) |
156 |
| Such bodies in the store | 2,466 (ahu-ocr / chat) |
| Whole store | 20,858 rows · 278 MB compressed · from 2026-07-04 |
The leak path is the LLM extraction prompt: it inlines the full OCR'd document text. By contrast operation=ocr and operation=classify bodies are ~400 bytes and clean — the binary-stubbing work is doing its job. So this is one specific path, not a general failure.
Why it matters: a data subject exercising Art 8 (erasure) or Art 9 (end processing) cannot be fully served. The OCR app can delete its own records — delete routes exist across all flows — but the audit copy survives, by design, forever.
Recommended fix — crypto-shredding. Encrypt PII-bearing bodies with a per-document key held in a separate, deletable keystore. Erasure then destroys the key, not the row: the hash chain stays verifiable over ciphertext, immutability is preserved, and the plaintext is unrecoverable. This is the standard resolution to the immutability-vs-erasure conflict and it keeps both properties instead of trading one away.
Cheaper alternatives, in case that is too much: (b) truncate body plaintext after N days, retaining full_body_sha256 so the chain still verifies; (c) stop storing request bodies for operation=chat on document engines and keep only the hash. (c) is nearly free and would have prevented this entirely — the audit value of the content of an extraction prompt is low compared to its liability.
Finding 2 — retention is declared but not implemented
RetentionDays (default 400) appears in internal/config/config.go:30,581 and deploy/observatory.example.yaml. It appears nowhere else — no deletion code, no TimescaleDB add_retention_policy, no drop_chunks. Nothing expires.
Art 43(2) requires erasure once the retention period ends or the purpose is fulfilled. A knob that reads as compliant while doing nothing is worse than no knob: it will be cited as evidence of a control that does not exist. Either implement it or delete the setting.
Nothing has aged out yet (oldest row 2026-07-04), so there is no backlog to clear — implement it now and the problem never accrues.
Finding 3 — no DPIA, and one is mandatory
Art 34 requires a DPIA for high-risk processing. This deployment hits at least three of the enumerated triggers: specific personal data (health, financial), automated decision-making with legal effect — an OCR+LLM pipeline that classifies and extracts filings for a government registry — and large-scale processing.
Art 10 also gives data subjects the right to object to decisions based solely on automated processing. Relevant here in a concrete way: we now know the classifier mislabels documents at ~13% on distinct deeds while reporting 0.96+ confidence, and that confidence scores are inverted (see the OCR quality findings). A DPIA is the correct place to record that a human verification step is load-bearing rather than decorative.
Finding 4 — no governance artifacts exist
A repo-wide search for PDP/DPIA/retention/privacy-notice material returns only commercial proposals that mention PDP. There is no:
- Record of processing activities (Art 31) — what data, what basis, what retention, who receives it
- Privacy notice / lawful basis per flow (Art 20) — notaris upload documents containing third parties' data; the basis for processing those third parties needs to be stated
- DPO appointment (Art 53) — required for public-service processing, large-scale processing, or specific data. This deployment is all three
- Breach-notification runbook (Art 46) — 3×24 hours to the agency and affected subjects. Detection improved a lot today (EWS → Keep now pages a real mailbox) but detection is not notification: there is no documented decision path, no template, no named owner
- Processor agreement (Art 51) between Ditjen AHU as controller and us as processor
These are documents, not engineering. They are also the first thing an audit asks for, and the cheapest items on this list.
Finding 5 — real personal data in staging
Staging holds 177 real KTP records and real notarial deeds, and the ahu-ai-ocr-web container has SECURITY_DEV_LOGIN_SECRET configured. The dev-login path is secret-protected, so this is attack-surface reduction rather than an open hole — but a development authentication path in an environment holding real citizens' identity documents is the kind of finding that reads very badly in an audit. Either use synthetic/redacted fixtures in staging, or drop the dev-login secret from that environment.
What is already right (do not lose these)
Credit where due — several of these are things most projects get wrong:
- No cross-border transfer.
OCR_PROVIDER=azure-on-prem, vLLM on-prem, gateway enforceson_premupstreams and production refusesexternal_dev. Art 56 is the requirement that most commonly blocks government AI, and it is designed out rather than mitigated. - Authorization is enforced.
SECURITY_ENFORCE=onin the running staging container — I expected this to be off and it was not. - Body access is itself audited.
body_access_auditrecords reads of stored bodies — exactly the control Art 35 expects around sensitive stores. - Erasure is possible app-side. Delete routes exist across all 13 flow route modules.
- Audit integrity. Hash-chained, tamper-evident, with verified rejection of UPDATE/DELETE.
- Secrets discipline. No credentials in repos; env-var indirection throughout.
Remediation order
| Priority | Action | Type | Effort |
|---|---|---|---|
| 1 | Stop storing operation=chat request bodies for document engines (or crypto-shred them) |
eng | S–M |
| 2 | Implement retention_days, or remove the knob |
eng | S |
| 3 | Stop extracting golongan_darah — nothing needs it |
eng | XS |
| 4 | Breach-notification runbook (3×24h, named owner, template) | doc | S |
| 5 | RoPA + privacy notice + lawful basis per flow | doc | M |
| 6 | DPIA covering specific data + automated decisioning | doc | M |
| 7 | Appoint/designate a DPO | org | S |
| 8 | Synthetic data in staging; drop dev-login there | eng | S |
| 9 | Processor agreement with Ditjen AHU | legal | M |
Items 1–3 are days of engineering. Items 4–7 are documents that can be drafted in parallel and are what an audit actually asks to see first.
Exposure if ignored
Administrative sanctions (Art 57): written warning, temporary suspension of processing, an order to delete personal data, and fines up to 2% of annual revenue. A suspension order against the processing itself would stop the platform. Criminal provisions (Arts 67–68) reach individuals for unlawful obtaining, disclosure or use, with corporate fines multiplied under Art 70(2).
The realistic risk is not a raid. It is a procurement questionnaire — for the KLH deal or the next AHU contract — asking for the DPIA, the RoPA, and the retention policy, none of which exist today.
What I did not assess
Encryption at rest for the uploads volume and the databases; TLS posture between services; the chatbot's conversation-retention behaviour; whether notaris consent language upstream in SABH covers our processing; and the six non-GPU hosts. Each of those could add findings.