People
Download your data
Anybody can have a copy of what they have put here: everything they wrote, the pictures and videos they posted, who they follow and who follows them. They ask for it in their settings; a worker gathers it into one zip file; they fetch it within a week.
Its routes are in the API reference: export-service.
The parts
| Part | What it does |
|---|---|
export-service (port 8104, /exports) |
Takes a request, says how it is getting on, and gives the address the archive is fetched from |
export-worker |
Makes each archive, stores it, and deletes the ones that have been kept their time. Answers no requests |
data_exports |
One row for each request: the request, the work on it, and the record of it |
graph-grpc-service |
ListAuthored, the list of everything one person wrote |
file-grpc-service |
Keeps archives under assets/v1/export-service/, and only ever sends one as a file to save |
One module, export-service, with two programs and two images, as the
video service is.
Asking for one
POST /exports, by somebody who has just proved who they are at
auth-service's /reauth/ endpoints, the same proof deleting a profile
asks for. An archive holds what only its owner may see, and a session left
open on a shared computer must not be enough to take it. One proof makes
one export. Where the proof cannot be checked, because Redis is not
reachable, the request is refused.
| Refused with | When |
|---|---|
403 step_up_required |
No fresh proof |
409 active |
One is already waiting or being made. One at a time for each person, which is a rule of the table (a unique index) and not of whichever request arrived first |
429 too_soon, with next_at |
The last one was asked for less than EXPORT_EVERY_DAYS ago. One that failed or was given up does not count |
Where the request came from, its address and browser, is kept on the row.
What is in an archive
| File | What |
|---|---|
index.html |
Everything here as a page to read. It has its own styles, no scripts, and fetches nothing: it works with no connection |
README.txt |
What each file is, and how many posts and files there are |
data/profile.json |
The profile |
data/posts.ndjson |
Everything written, one post on each line: notes, articles, topics, photos, videos, replies, reposts and custom feeds, wherever each was posted, including what only its author may see |
data/following.json, followers.json, blocked.json |
Who they follow, who follows them, who they blocked |
data/liked.json, saved.json |
What they liked and saved, by name and address |
actor.json, outbox.json |
The profile and the posts as ActivityStreams, the form other fediverse software reads. What was not public is addressed to nobody |
media/<post id>/ |
Each post's pictures at the largest size kept, a video at its largest size, an article's cover |
media/profile/ |
The avatar and the cover |
Every name and every post is somebody's text, and is written into the page as text: the page is made with a template that escapes all of it.
What is not
- What other people wrote. A reply names the post it answers and a repost the post it reposts; neither holds it.
- What was removed. It is gone for its author too.
- The account: the email address, sign-ins, passkeys. Those are auth-service's, which this does not read.
- Files storage does not have. A post naming a picture that is not
there keeps its text; the file is left out and counted in
README.txt, so one missing picture does not cost somebody their archive.
How it is made
- A worker takes the oldest request waiting (
SELECT … FOR UPDATE SKIP LOCKED, so several workers each take a different one). - It reads the person's data through the graph, as that person, a page of 100 at a time, and writes the archive to a file of its own as it goes. Nothing is held whole in memory: an archive is as large as everything somebody posted.
- Pictures and videos are copied in from storage as they are, not compressed again.
- The file is stored under
assets/v1/export-service/<person id>/<export id>.zip, and the request is markedready.
Between each page and each file the worker asks whether the request has been given up, and stops if it has. One given up in the moment it was finished is deleted and not marked ready.
A worker that dies leaves its request taken; another takes it after two
hours. A request is tried three times, then marked failed.
Nothing that is in an archive is logged: only that one was made, how big it is and how many posts and files it holds.
On the development data, an account with 8,410 posts and 840 MB of pictures took five and a half minutes.
Keeping and deleting
An archive is kept for EXPORT_KEEP_DAYS, a week unless said, and then
deleted from storage by the worker, the file first and the row marked
expired after. Its owner can delete it sooner.
It is fetched from an address signed for a few minutes, made afresh each
time the export is read, which sends it as a file to save. file-grpc-service
sends anything under that folder that way and no other, whatever is in it.
Endpoints
All of them are the viewer's own: an export belongs to whoever asked for it, and anybody else is told there is none.
| Method and path | What |
|---|---|
POST /exports |
Ask for one |
GET /exports |
The latest ten, with next_at when it is too soon to ask again, every_days and keep_days |
GET /exports/:id |
One, with download_url while it is ready |
DELETE /exports/:id |
Give it up while it is being made, or delete its archive |
status is pending, building, ready, failed, cancelled or
expired.
Settings
| Variable | Value |
|---|---|
EXPORT_EVERY_DAYS |
Days somebody waits between one export and the next. 7 unless said; 0 for no wait |
EXPORT_KEEP_DAYS |
Days an archive is kept. 7 unless said |
PUBLIC_WEB_URL |
Worker: the site's address, for the links in an archive back to where each thing is |
SITE_NAME |
Worker: the site's name, for the page's heading. Anahita unless said |
EXPORT_WORK_DIR |
Worker: where an archive is made before it is stored. /work in the image. Needs room for the largest one |
Running it, and not running it
make export builds and starts both containers. They need the database,
Redis, graph-grpc-service and file-grpc-service.
Both are optional. Without them the web app's setting says the download is not available here.
Where the code is
| What | Where |
|---|---|
| Asking for one, and the endpoints | export-service/cmd/api/handlers/exports.go (Add), over anahita-libs/sessions/step_up.go (RequireStepUp) |
| The requests | export-service/data/repositories/exports.go (Claim, MaxAttempts), table data_exports in migration-job/schema/init.sql |
| The worker: making, storing, deleting | export-service/worker/worker.go (Tidy), export-service/cmd/worker/main.go |
| Reading as the person | export-service/worker/source.go |
| What is in an archive | export-service/archive/archive.go (Write), export-service/archive/page.go (writePage for index.html, readme, actorOf and outboxOf) |
| Everything one person wrote | ListAuthored: graph-grpc-service/cmd/server/handlers/saved.go, anahita-libs/data/services/media_authored.go |
| Sent only as a file to save | file-grpc-service/data/services/uploads.go (ExportsPrefix) |
| Settings | export-service/config/config.go: EXPORT_EVERY_DAYS, EXPORT_KEEP_DAYS, PUBLIC_WEB_URL, SITE_NAME; EXPORT_WORK_DIR in export-service/cmd/worker/main.go |
| Tests of the rules | export-service/cmd/api/handlers/exports_test.go, export-service/archive/archive_test.go, export-service/worker/worker_test.go, export-service/data/repositories/exports_integration_test.go |
| The web app | src/containers/actors/Settings/DataExport.jsx, src/api/exports.js (anahita-web) |
What is not built
- Telling the person, by a notification or an email, that their archive is ready. The settings page says.
- The account's own data, from auth-service.
- A Bluesky repository file, which comes with federation.
- Deleting somebody's archives when their account is purged. They are deleted when their week is up.
From anahitasocial/anahita-services, 2026-10-27.
Written by Claude, reviewed by Rastin Mehr · About this