Skip to content

Data exports

A user requests an export from the Data section of their account settings, at /account/data. The worker builds an archive asynchronously and emails a download link; the link resolves at /account/exports/{id}/download.

Nothing happens without a running worker. The request returns 200, the row is created, and the archive is never built. See What runs in production.

Export archives are personal data, and the application never exposes its storage bucket to a browser. It writes every object privately and generates no public and no presigned URL — the download route streams the bytes itself, behind a link that requires the authenticated owner plus a SHA-256 token, expires 48 hours after the export completes, and answers 404 on any mismatch.

An hourly scheduled command, app:purge-expired-exports, deletes expired archives and their rows. Like everything else on the schedule, it runs only if a worker is consuming.

What the archive contains

comments.json includes the user’s comments and replies, including retained deleted threads. Each row carries deletedAt, which is the thread’s deletion time or null for an active thread. Replies use their parent thread’s deletion time. The export preserves comment text, anchors, and review status.

cards.json holds the board cards of the projects the user owns. A card that belongs to an epic carries parentCardId and parentNumber, which name that epic. A card with no epic carries null in both. Each card also carries laneEnabled, which says whether the board draws an epic as a lane. Only an epic reads it, and every other card carries the default, true.

Each card in cards.json also carries feedback, the site-review notes on the card. Each item gives the id, the page url, the page context, the anchors, the body, the drawing strokes, the status and createdAt. The strokes are the full vector points. The archive has no separate site-review file.

card-verdicts.json holds the verdicts the user sent from the site-review widget. Each item gives the id, the cardId, the kind, the message, the copied notes, the createdAt and the deliveries. A delivery names the pull request, its state, its reason, the reviewUrl and the settledAt. The file never holds a GitHub token.

waitlist.json holds the waitlist entry of the user, if one exists. It gives joinedAt, the picks the user chose and the other text. Account deletion deletes the entry.

beta_invite.json holds betaTesterSince, the date the user redeemed a beta invite. It is empty for a user who redeemed none. The admin’s note on the invite stays out of the export.

worker_run_facts.json holds the metrics row of each worker run of the projects the user owns, with its outcome, its duration, its cost, its token sums, its timing, its peak context, its harness and its account. It includes the rows whose run the retention sweep deleted. worker_run_tool_calls.json holds each tool call of those runs, with its tool, its kind, its timing and its signatures. worker_run_bucket_times.json holds the time of each run in each bucket of the project rules. The metrics rows also hold the host values of each run: its mean CPU use, its peak memory and swap, its concurrent runs and its battery flag. bridge_host_samples.json holds each host sample of the user’s bridges, with its bridge, its time, the use of each core, its memory and swap, its battery charge and its power source. Retention lists the other worker run files.

insights_analyses.json holds the analyses of the projects the user owns, each with its proposals. insights_project_settings.json holds the analysis settings of those projects: the default model, the default effort, and whether the bridge collects the full text of each tool call.

artifacts.json holds the artifacts of the projects the user owns, archived ones included. Each artifact lists its versions, and each version lists its files. A text file carries its content as a string. Any other file, and every image, carries it as base64.

The archive holds one file per kind of data. audit_log.json is one of them. It holds the audit records the user is the actor of, and the records that name the user as the subject. What was done to the account is the account’s data too.

A subject record was written by somebody else, so the file names no actor at all. It carries the operation, the outcome, the category, the channel, the subject and the context of each record, and never an actor name.

An administrator’s export contains the identifiers of the accounts they acted on, because those records are the administrator’s own actions. An identifier is an opaque UUID. It carries no name and no address.

That file reaches back as far as the trail does, which is 180 days by default and is set by the audit.retention_days feature flag. See The admin area.

Where archives are stored

EXPORT_STORAGE is local by default, which is correct only when the process that generates an export and the process that serves its download share a filesystem. Separate web and worker containers do not, and there local means every download 404s. See Object storage.

Archives need no backup: they are derived data, and a user can ask for a new one whenever they want. See Backing up.