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.
What the link guarantees
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.