Every file a workspace keeps in object storage has one row in stored_objects:
its key, its workspace, its size as the server measured it, and what kind of
file it is. A workspace's usage is the sum of those rows (staged ones
excepted, below), and it is checked
against the plan's max_storage_gb (StorageQuotaService.assert_storage_available).
services/stored_objects.py is the only thing that writes the table:
| Call | When |
|---|---|
record(...) | The server has just written the bytes, and knows their length |
record_from_storage(...) | A browser or device uploaded straight to storage through a presigned URL; the size is read from storage |
claim(keys) | A staged upload is attached to what it was uploaded for |
remove(keys) | Delete the objects and their rows together |
remove_after_commit(keys) | The same, for deletes inside someone else's transaction: rows now, objects once it commits |
key_in_use(key) | Whether any record still points at an object; asked before deleting a file other rows may share |
A key is counted once however many rows refer to it (a ticket upload mirrored onto its task, a duplicated document's attachments, a form upload that became a resume).
What is counted#
| Kind | Written at | Checked before upload |
|---|---|---|
drive | Drive upload | yes |
task_attachment | task attachment upload | yes |
compliance_document | direct upload, or registration after a presigned upload (sized from storage; the key must be the workspace's) | yes (presign: the declared size) |
docx_version | every saved version of a Word document | yes, including duplicate and version restore |
document_attachment | document attachment upload | yes |
import_archive, import_attachment | Confluence/Notion import, and the files it extracts | yes (the archive) |
chat_file | chat upload | yes |
ticket_attachment | files added by agents or members | yes |
public_form_upload | public form file field, staged until the form is submitted | never refused |
assessment_resume | resume on an assessment's shareable link, staged until kept | never refused |
assessment_answer | a candidate's file answer | never refused |
assessment_recording | webcam and screen recordings, sized from storage on completion | never refused |
tracker_evidence | desktop tracker screenshots, sized from storage when the event arrives | yes (the declared size) |
People outside the workspace (candidates, form submitters, ticket requesters)
are never refused because of the workspace's plan. Their files count all the
same — except a staged upload (public_form_upload, assessment_resume),
which counts only once it is claimed. Anyone with a public form or a shareable
link can stage one without signing in; counted at once, a stranger could push
the workspace past its limit and have its own team's uploads refused until the
cleanup removed what was never submitted.
What is removed#
- Staged uploads nothing claimed within a day, import archives once
the import completes (a week after a partial or failed one, for the retry),
and multipart uploads abandoned for a day: the
cleanup_stored_objectsactivity, hourly (stored-object-cleanup). Before removing a staged upload it checkskey_in_use, and claims it instead if something did attach it. An abandoned multipart upload (a candidate who reloaded mid-recording) is not an object, so neither the ledger nor a listing sees it; aborting it is what frees its parts. - Word history: each document keeps the newest
keep_versionssaves (workspace setting, Settings → Docs → Word version history, default 20, 1 to 200). Older versions and their files are deleted as new ones are saved; pinned and labelled versions are always kept and do not count toward it. Purging a trashed document removes every version's file. - Drive: deleting a file deletes its bytes. There is no Drive trash to restore from, so a deleted file used to stay stored and uncounted.
- Tickets: deleting a ticket deletes its files and its replies' files, unless the linked task or anything else still holds them.
- Recordings: Delete recording on a candidate's proctoring tab, workspace admins only. The proctoring events and trust score stay; a recording whose file could not be deleted keeps its link.
Not removed: compliance documents marked deleted (kept, and counted, as before), chat files (no record says which message holds which file), and archived tasks (they can be restored).
Backfill#
migrate_2026_09_28a_stored_objects.sql fills the table from every table that
records a key and a size. Then list the bucket to add what nothing records and
correct sizes a browser reported:
docker exec aexy-backend python scripts/backfill_stored_objects.py --dry-run
docker exec aexy-backend python scripts/backfill_stored_objects.py
--prune-missing forgets rows whose object is gone. --purge-deleted-drive
deletes the files of Drive items deleted before deleting freed storage.