Skip to content
Knowledge sections

Workspace data export and GDPR requests

How an owner exports a workspace as a gzipped JSON document, and how that export plus account deletion serves a data subject request.

exportgdprgovernance
On this page

A Workspace Export is one JSON document holding everything a Workspace does, requested by an owner from workspace settings and built by the background worker. It exists for two reasons: an owner leaving the product should be able to take their data with them, and a GDPR access request should be answerable without a database console.

Status: built and persisted in the Reference Application and the API Worker (ADR 0055). Requires an R2 bucket at deploy time; local development uses an in-memory adapter that builds the same document.

What the document contains

The export is a single self-describing JSON document, stored and served gzip-compressed: application/gzip, named <slug>-export-<exportId>.json.gz. Decompressed, it is UTF-8 JSON with these fields:

FieldContents
readmeSchema version, generation time, and a description of every field below
workspaceThe workspace record (id, slug, name, planId)
membersEvery Member with their Workspace Role and system role
invitationsEvery Invitation, pending or settled
apiTokensAPI Token metadata: id, name, prefix, scopes, timestamps. Never the secret or its hash
webhookEndpointsWebhook Endpoints without their signing secret, each with its recorded deliveries
auditEventsThe complete workspace Audit Event trail, newest first
notificationsWorkspace-wide Notifications. Notifications addressed to one user are that user's data and are not included

Every list is the same projection the application renders, so the export cannot contain a field the UI hides.

Requesting an export

  1. An owner opens workspace settings and clicks Request export. The permission is workspaceExport:request, granted to owners only: an admin can rename the workspace but not walk away with every member's email and the full audit trail.
  2. The app writes a pending export, records the Audit Event workspace.export_requested, and enqueues a job.
  3. The background worker snapshots the workspace through the capability services, builds the document, gzips it, writes it to the export bucket, marks the export ready, records workspace.export_completed, and creates a Notification for the requester.
  4. Settings lists the export with a download link. The link is signed and valid for fifteen minutes; the artifact itself is kept for seven days, after which the bucket's lifecycle rule deletes it and the row reports it as expired.

Every download records workspace.export_downloaded.

How a download works

The link points at the API Worker: GET /exports/<exportId>/download?expires=<unix>&signature=<hex>. The signature is an HMAC over the export id and the expiry with a secret minted for that one export and stored on its row, so the web and API workers share nothing but the database, and a leaked link opens one artifact for at most fifteen minutes. An unknown id, a tampered signature, and an expired link all answer the same 404.

Machine clients hold an admin-scoped API Token and use the REST surface: POST /workspaces/:slug/exports requests one, POST /workspaces/:slug/exports/:exportId/download-link mints the same signed URL after re-checking workspaceExport:download.

When exports are unavailable

The export queue and R2 bucket are an Optional Provider. Set WORKSPACE_EXPORT_BUCKET at deploy time and alchemy.run.ts provisions both with their bindings on all three workers. Unset, the settings card explains that exports are not configured and shows no button; nothing else degrades. Set API_PUBLIC_URL on the web worker so download links point at the deployed API worker; unset, they point at the local dev server on port 8787.

Serving a data subject request

A GDPR request from a person has two halves, and the starter serves them with two features that stay separate on purpose.

Access (Article 15). The Workspace Export is the workspace's data, requested by the workspace's owner. When the data subject is a Member, the owner exports the workspace and hands over the parts that concern the person: their members entry, the Audit Events where they are the actor or the target, and any Invitation addressed to their email. When the data subject is not a Member (an invitee who never accepted), the invitations list is the only place they appear. The export never contains another user's personal notifications, so the owner does not have to redact them.

Erasure (Article 17). Account deletion, owned by the account settings surface, removes the person's user row and, through the database's cascades, their memberships, sessions, and personal notifications. It does not rewrite the Audit Event trail: rows where the person was the actor keep their actorUserId for as long as the log's retention says, because a governance record that forgets who acted is not a governance record. That retention is the deployment's decision to document in its privacy notice.

Serving a full request, in order: export the workspace first so the access copy predates the erasure, hand over the relevant parts of the document, then delete the account. Exports are workspace-scoped, so a person who belongs to several workspaces is served by an export from each. Neither feature is a substitute for the other: an export does not delete, and a deletion does not produce a copy.

What the starter leaves to you

Retention of the audit trail, the privacy notice that describes it, identity verification before acting on a request, and a personal (per-user) export across workspaces. The last one is a natural extension: the same collectWorkspaceExportSnapshot could be filtered to one actor and run over listWorkspacesForUser.