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:
| Right | Article | Scope | Surface |
|---|---|---|---|
| Data portability | Art. 20 | User and org | Authenticated API |
| Right to erasure | Art. 17 | User and org | Authenticated 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:
| File | Contents |
|---|---|
user-profile.json | The requesting person's profile. If this read fails, the whole export fails. |
conversations/<id>.json | One file per conversation, with its messages |
api-keys.json | Key metadata only (name, prefix, created, last used, scopes). Never key material. |
audit-log.json | Security events for the person (user scope) or the org (org scope) |
members.json, workspaces.json | Org 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
POST /v1/:org_slug/:workspace_slug/privacy/export
Content-Type: application/json
{ "scope": "user" }{ "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:
GET /v1/:org_slug/:workspace_slug/privacy/export/:exportId{ "exportId": "…", "status": "ready", "ready": true, "storageKey": "…", "completedAt": "…" }GET /v1/:org_slug/:workspace_slug/privacy/export/:exportId/downloadThe 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.
What happens at request time
- 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. - Oxagen writes a
privacy.erasure_requested(user scope) orprivacy.org_erasure_requested(org scope) security event. - Oxagen dispatches a
privacy/erasure.executejob forscheduledAt, 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.invalidaddress, 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_byandupdated_byreferences
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
POST /v1/:org_slug/:workspace_slug/privacy/erase
Content-Type: application/json
{ "scope": "user", "confirm": true }{ "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:
{
"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:
| Action | Event type |
|---|---|
| Export requested | privacy.export_requested |
| User erasure requested | privacy.erasure_requested |
| Org erasure requested | privacy.org_erasure_requested |
These events appear in the audit log, and you can use them as evidence in GDPR audit responses.