Fulfil a per-customer data export for legal compliance
Symptom
You receive a legal-compliance request for a full data export of one customer org — GDPR Article 15 (right of access), subpoena, regulator audit, or pre-purge archive before an archive-unpaid-org window closes. The customer may or may not be the requester; either way, the deliverable is a single verifiable archive you hand off to Novalien Legal.
Likely causes
- GDPR Article 15 “subject access request” — customer Owner asks for a copy of all org data.
- Subpoena or regulator order — Novalien Legal is the requester, not the customer. The customer may not even know the request exists.
- Pre-purge archive — the org is in the 90-day
disabledwindow before hard-delete and the customer asked for their data before it is gone.
Fix
Scope decision — what is “the customer’s data”
| Category | Included by default | Location |
|---|---|---|
| Users (email, display name, roles, site access, last_login) | yes | users table |
| Sites, cameras, gateways config | yes | sites, cameras, gateways tables |
| Events (metadata + AI labels) | yes | events table |
| Clip recordings (MP4) | yes — by far the largest | GCS bkt-cloud-ai bucket |
| Alert rules, prompt packs, webhooks | yes | respective tables |
| Audit log (90 days) | yes | audit_log filtered by org_id |
| Platform audit rows about this org | only if Legal says yes | platform_audit_log filtered by target_org_id |
| Billing PII (card last-4, address) | never — Legal handles separately via Stripe | — |
Step-by-step
-
Confirm the requester and the scope in writing from Legal. Do not start an export on a customer email alone. A subpoena must be acknowledged by Legal first.
-
Mint a scoped impersonation token for the org (US-PLAT-8) only if you need to verify data before export. The export script itself runs with a platform JWT and direct DB access — it does not need impersonation. Reason string for any impersonation:
"verify data export ticket=<id>". -
Run the export script from the ops host:
Terminal window cd cloud && go run ./cmd/customer-data-export \--org-id=<org_id> \--ticket-ref=<ticket> \--include-platform-audit=<true|false> \--out=/var/exports/<org_slug>-<date>.zipThe script writes one ZIP containing JSONL files per table plus a
clips/subdirectory with MP4s named<event_id>.mp4. A top-levelMANIFEST.jsonlists every file, its SHA-256, and the query used to produce it.
-
Verify the manifest. Row counts per table in the manifest must match
SELECT count(*)run directly against the DB under the same filter. A mismatch means the export is incomplete; rerun. -
Deliver. Upload the ZIP to the secure handoff location Legal specifies. Include the
ticket_ref, the manifest SHA-256, and the export window (UTC timestamps). Never email the archive directly.
- Audit. Verify
platform.data_export_completedexists inplatform_audit_logwithtarget_org_id,ticket_ref, and the manifest hash. Also writeorg.data_exported_by_platforminto the target org’saudit_logso the Owner sees it on their next login — unless Legal has specifically ordered the export be kept hidden from the customer (subpoena).
Verify
- The ZIP extracts cleanly and the MANIFEST.json hash matches what you reported to Legal.
- Row counts match the DB for every included table.
- Both audit logs show one row each for the export (or platform-only if Legal suppressed customer-side visibility).
- No row in any exported file belongs to a different
org_id— cross-tenant leak check.
If none of this worked
- If the script fails mid-export, delete the partial ZIP and rerun — do not deliver a partial archive. The script is idempotent.
- If clip download is rate-limited by GCS, run the clip half with
--clips-onlyand the data half with--no-clipson separate hosts. - Related: GDPR deletion when the request is to erase rather than export.