Contents · Secure

Docs / Secure

GDPR + Malaysia PDPA

Status: engineering controls map — not a certification, not legal advice.
EdgeCMS implements concrete privacy hooks (inventory, export/delete, retention, consent recording, residency notice). Counsel must still review the draft legal pack under apps/website/legal/ before any customer-facing claim.

Companion code: apps/api/src/privacy/. Machine inventory: GET /api/privacy/inventory. Notice: GET /api/privacy/notice.

Scope (honest)

In this sliceOut of this slice
Data inventory of what the Worker storesDPIA, RoPA as a legal filing, ISO/SOC attestation
HS-13 user export + deletion (already shipped; extended)Field-level PII search/redaction inside entry JSON
Entry version retention hooks (keep-N / max-age)Automated workspace wipe after 30 days (HS-09 Planned)
SU-04 legal-acceptance API hooks + D1 tableSignup checkbox UI / website wiring (SU-04 Planned)
Declarative data-residency notice (env / host policy)Pinning Cloudflare D1/R2 to a single country
Audit retention ≥365 days (HS-12)Erasing host SIEM copies, Cloudflare logs, backups
Link to draft Privacy Policy / DPA / subprocessorsFilling [COMPANY] placeholders or publishing the drafts

Malaysia PDPA 2010 (as amended) and GDPR share notice, purpose limitation, access/correction, and deletion themes. Where PDPA requires a specific consent form, the draft Privacy Policy is the notice and legal_acceptances records the tick — once signup wires SU-04.

Roles

RoleWhenDocument
ControllerAccount, sessions, memberships, billing contact, website, audit of platform actionsPrivacy Policy
ProcessorPersonal data customers put in entries, assets, AI contextData Processing Addendum

Customers remain the controller for content they store. EdgeCMS does not invent a legal basis for that content; the customer does.

Data inventory

Authoritative list lives in code (PRIVACY_DATA_INVENTORY) and is served at GET /api/privacy/inventory. Summary:

CategoryRoleRetentionDeletion coverage
Account profileControllerLifecycle of accountHS-13 user deletion
Credentials / MFAControllerLifecycleHS-13 (export omits secrets)
Sessions (incl. IP / UA)ControllerUntil expiry or deletionHS-13
MembershipsControllerLifecycleHS-13 (sole-owner blocked)
Legal acceptancesControllerLifecycle (proof of notice)HS-13
Authored-by metadataProcessorFollows content rowPseudonymized on HS-13
Audit eventsController≥365 days (AUDIT_RETENTION_DAYS)Pseudonymized; pruned by retention
Entry versionsProcessorMax 50 / 90 daysContent retention; not user wipe
Assets / live entriesProcessorLifecycle / HS-09 targetTenant offboarding Planned
AI context to subprocessorsProcessorProvider terms; AI off until enabledOut of HS-13 scope beyond created_by
Edge / SIEM / backupsControllerHost / Cloudflare policyOut of scope

Limits that stay true

  • Personal data typed into content fields is not searched or changed on user deletion.
  • Rows in a dedicated tenant D1 are not pseudonymized by HS-13.
  • Webhook payloads, operator logs and backups outside D1 are host-owned.
  • Pseudonymized ≠ anonymized: someone holding the old user id can still match audit digests.

Rights → product hooks

Right (GDPR Art. / PDPA theme)Hook
Access / portabilityGET /api/me/data-export (and admin twin); Settings → Account → Your data
ErasurePOST /api/me/data-deletion { "confirm": "DELETE" } — pseudonymize refs, delete account
NoticeDraft Privacy Policy; GET /api/privacy/notice; inventory endpoint
Consent / acceptance recordPOST /api/me/legal-acceptance { document, version } (SU-04 hook)
Restrict / objectManual / support process today — no product toggle
Breach noticePromised in draft DPA; operational runbook is host-owned

Details of export/delete behaviour: Operations and security — Personal data requests.

Retention

Central knobs: apps/api/src/privacy/retention-policy.ts (PRIVACY_RETENTION).

DataEnforced?Value
Audit ledgerYesMin/default 365 days
Entry versionsYesMax 50 per entry; drop older than 90 days (never current)
Account / workspace after closureNo (documented target)30 days then delete — needs HS-09
Billing / taxHost counselPlaceholder in Privacy Policy

Content hook used by write paths: contentRetentionHooks.enforceEntryVersions (wraps retentionService).

Drafts (not in force): apps/website/legal/.

DocumentProduct hook
Terms of Servicelegal_acceptances.document = terms_of_service
Privacy Policyprivacy_policy
DPAdpa (usually B2B countersign; still recordable)

POST /api/me/legal-acceptance is idempotent per (userId, document, version). Signup UI that requires the checkbox before account creation is SU-04 Planned — do not claim it is live on the website until that ships.

Locale and data residency

  • Content locales (localeCatalog, collection supportedLocales, public locale provenance headers) are editorial/i18n features, not a residency control.
  • ABAC locale attribute gates who may edit a locale; it does not move bytes.
  • Cloudflare runs a global edge. D1/R2/KV locations follow the Cloudflare account configuration. This product does not offer a per-tenant “store only in MY” switch.
  • Hosts set a customer-facing sentence via host.privacy.dataResidencyNotice or PRIVACY_DATA_RESIDENCY_NOTICE. Example: jurisdiction of the operating entity + SCC posture for EU transfers. Exposed at GET /api/privacy/notice.
  • Cross-border transfers for EU/UK customers need SCCs (or equivalent) with subprocessors — see draft Subprocessors. Malaysia PDPA transfer rules need counsel review for the operating entity.

Host composition

ts
createApp({
  host: {
    privacy: {
      dataResidencyNotice:
        'Operating entity in Malaysia; primary Cloudflare account region APAC; EU transfers under SCCs.',
      onPrivacyEvent: async (event) => {
        // enqueue ticket / SecOps notify — never block the request on slow I/O
        console.log(event.action, event.userId)
      },
    },
  },
})

onPrivacyEvent fires after a successful HS-13 export or deletion. Errors are logged and never fail the request.

IDStatusNote
HS-12 Audit retentionShipped≥365 days
HS-13 Personal data requestsShippedExtended with legal acceptances + host event
HS-09 Tenant offboardingPlannedNeeded for post-closure content wipe
HS-14 AI switchShippedNo AI subprocessors until tenant enables
SU-04 Accept ToS/privacy at signupPlannedTable + API ready; UI not
SP-07 CertificationsNot claimedKeep docs free of fake cert language

Tests

bun test --isolate \
  apps/api/src/tests/privacy/privacy.service.test.ts \
  apps/api/src/tests/privacy/privacy.controller.test.ts \
  apps/api/src/tests/privacy/data-inventory.test.ts \
  apps/api/src/tests/privacy/legal-acceptance.test.ts

Edit docs/public/gdpr-pdpa.md on GitHub