Skip to content

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

  1. GDPR Article 15 “subject access request” — customer Owner asks for a copy of all org data.
  2. Subpoena or regulator order — Novalien Legal is the requester, not the customer. The customer may not even know the request exists.
  3. Pre-purge archive — the org is in the 90-day disabled window before hard-delete and the customer asked for their data before it is gone.

Fix

Scope decision — what is “the customer’s data”

CategoryIncluded by defaultLocation
Users (email, display name, roles, site access, last_login)yesusers table
Sites, cameras, gateways configyessites, cameras, gateways tables
Events (metadata + AI labels)yesevents table
Clip recordings (MP4)yes — by far the largestGCS bkt-cloud-ai bucket
Alert rules, prompt packs, webhooksyesrespective tables
Audit log (90 days)yesaudit_log filtered by org_id
Platform audit rows about this orgonly if Legal says yesplatform_audit_log filtered by target_org_id
Billing PII (card last-4, address)never — Legal handles separately via Stripe

Step-by-step

  1. 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.

  2. 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>".

  3. 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>.zip

    The script writes one ZIP containing JSONL files per table plus a clips/ subdirectory with MP4s named <event_id>.mp4. A top-level MANIFEST.json lists every file, its SHA-256, and the query used to produce it.

  1. 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.

  2. 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.

  1. Audit. Verify platform.data_export_completed exists in platform_audit_log with target_org_id, ticket_ref, and the manifest hash. Also write org.data_exported_by_platform into the target org’s audit_log so the Owner sees it on their next login — unless Legal has specifically ordered the export be kept hidden from the customer (subpoena).

Verify

  1. The ZIP extracts cleanly and the MANIFEST.json hash matches what you reported to Legal.
  2. Row counts match the DB for every included table.
  3. Both audit logs show one row each for the export (or platform-only if Legal suppressed customer-side visibility).
  4. 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-only and the data half with --no-clips on separate hosts.
  • Related: GDPR deletion when the request is to erase rather than export.