oxagenDocs
Privacy

GDPR rights

How Oxagen supports GDPR Article 17 (right to erasure) and Article 20 (right to data portability).

Overview

Oxagen gives you self-service controls for the two GDPR rights with the most operational weight:

RightArticleScopeSurface
Data portabilityArt. 20User and orgAuthenticated API
Right to erasureArt. 17User and orgAuthenticated API

The current app has no Privacy settings page. The API behavior below is the implemented workflow. Contact privacy@oxagen.sh for requests that require completion beyond these handlers.

Both handlers require a resolved person. A plain API key resolves without one and is rejected. A browser session or a CLI-session key can carry the resolved person. See Authentication. Authorization and scope checks still apply.

Data export (Article 20)

An export produces a ZIP archive with:

FileContents
user-profile.jsonThe requesting person's profile. If this read fails, the whole export fails.
conversations/<id>.jsonOne file per conversation, with its messages
api-keys.jsonKey metadata only (name, prefix, created, last used, scopes). Never key material.
audit-log.jsonSecurity events for the person (user scope) or the org (org scope)
members.json, workspaces.jsonOrg scope only

Every artifact read must succeed before Oxagen uploads the archive or marks it ready. If a read fails, the job retries. If retries fail, the request is marked failed; it does not publish an incomplete archive.

Oxagen assembles exports in the background. The request returns at once with status: "queued". Poll GET /v1/:org_slug/:workspace_slug/privacy/export/:exportId as the job moves through processing to ready (or failed). You can poll only your own exports, and only in the organization that governed them.

The status read returns no URL. Oxagen stores the archive as a private object, so there is no address a browser or a client could fetch on its own. When the status says ready, fetch the archive from GET /v1/:org_slug/:workspace_slug/privacy/export/:exportId/download, which checks who you are and streams the bytes. It answers 409 while the archive is still being written or after the job failed.

API

http
POST /v1/:org_slug/:workspace_slug/privacy/export
Content-Type: application/json

{ "scope": "user" }
json
{ "exportId": "…", "status": "queued" }

An org-scope export sends { "scope": "org", "orgId": "<uuid>" } and requires the Owner or Admin role on that org. The orgId must be the organization the request is made in.

Poll, then fetch:

http
GET /v1/:org_slug/:workspace_slug/privacy/export/:exportId
json
{ "exportId": "…", "status": "ready", "ready": true, "storageKey": "…", "completedAt": "…" }
http
GET /v1/:org_slug/:workspace_slug/privacy/export/:exportId/download

The download responds with the ZIP, Content-Disposition: attachment, and Cache-Control: private, no-store.

Capability

export_data (domain privacy). The in-app agent can call it without an approval card.

CLI

The CLI has no privacy commands.

Account and org erasure (Article 17)

Erasure has two scopes. Any signed-in person can request erasure of their own account (scope: "user"). An org-scope request (scope: "org") requires the Owner role. Admin is not enough. In the in-app agent, erase_data always pauses for an approval card before it runs.

Loading diagram

What happens at request time

  1. Oxagen records the erasure request with status: "queued". In the same transaction, it deletes every active sign-in of the person who asked, so their access ends at once.
  2. Oxagen writes a privacy.erasure_requested (user scope) or privacy.org_erasure_requested (org scope) security event.
  3. Oxagen dispatches a privacy/erasure.execute job for scheduledAt, the end of the grace period.

What the erasure job does

For user scope, once the grace period has passed, the job:

  • anonymises the person's identity row: display name Deleted User, email replaced with an @deleted.invalid address, avatar removed
  • deletes the person's linked accounts, including OAuth tokens and the password hash
  • deletes the person's preferences

For org scope, the job erases no data.

The job then marks the request failed, not completed. This is deliberate. The full cross-store cascade is not implemented, so personal data can remain in these places:

  • ClickHouse telemetry rows that store the person's email
  • Neo4j nodes the person owns (there is no delete-by-owner path)
  • generated-asset blobs
  • Postgres created_by and updated_by references

Oxagen does not tell a data subject that erasure is complete while data remains. A failed request is the signal for an operator to follow up.

Grace period

The default grace period is 30 days (PRIVACY_ERASURE_GRACE_DAYS=30). Set it to 0 for immediate erasure in test environments. A missing or unparseable value falls back to 30. The grace period lets you cancel an accidental request: contact privacy@oxagen.sh before it ends.

API

http
POST /v1/:org_slug/:workspace_slug/privacy/erase
Content-Type: application/json

{ "scope": "user", "confirm": true }
json
{ "requestId": "…", "status": "queued", "effectiveAt": "2026-07-09T…" }

confirm must be the literal true. An org-scope request also sends orgId.

Job event

privacy/erasure.execute is an internal Inngest event that drives the job above. Its payload is:

json
{
  "requestId": "<uuid>",
  "userId": "<userId>",
  "orgId": "<orgId>",
  "scope": "user",
  "scheduledAt": "2026-07-09T..."
}

There is no customer-facing webhook subscription for this event.

Security events

Oxagen records every privacy request as an immutable security event in the security.security_events table:

ActionEvent type
Export requestedprivacy.export_requested
User erasure requestedprivacy.erasure_requested
Org erasure requestedprivacy.org_erasure_requested

These events appear in the audit log, and you can use them as evidence in GDPR audit responses.

On this page