Workspaces got avatars in v0.4.0. The feature is small: upload a picture, it shows in the switcher. The upload worked on the first try. The picture appeared everywhere it was supposed to. Every test passed.
It would also have vanished three days later.
Two ways to lose a file
Notez uploads go to object storage before anything in the database points at them. That is deliberate — the browser sends bytes straight to S3 with a presigned POST, so a large upload never occupies the API. The row describing the file is written first, then the bytes arrive.
That ordering creates a gap. If someone picks a file and closes the tab, the bytes may land with nothing ever referring to them. Storage fills with objects no note, no avatar, and no user will ever ask for again.
So there is a background job. Once a day it looks for files that were uploaded but never attached to anything, and deletes any older than three days. The word doing the work there is attached: a boolean the uploader does not set. Finishing an upload marks a file uploaded. Something else has to claim it.
For user avatars, that something is the profile update. When you set a new picture, the code does two things beyond writing the URL onto your account: it marks the new file attached, and it queues the file it replaced for deletion. Both halves matter. Without the first, the cleanup job reaps a picture that is actively in use. Without the second, every avatar you ever set stays in storage forever, counting against your quota.
Workspace avatars did neither. The URL was written to the workspace, the image rendered, and the file sat there marked uploaded, unattached, three days from deletion.
Why nothing caught it
The tests passed because they tested the thing we built. Upload returns a URL, the URL is stored, the component renders it — all true, all still true with the bug present. Nothing asserted anything about a boolean on a different table, because it had not occurred to us that a boolean on a different table was part of the feature.
The failure mode is also invisible in the moment. There is no error, no console warning, no failing request. The gap between the bug and its symptom is three days and one cron run, and the symptom is a broken image on somebody else's screen.
What surfaced it was reading the existing avatar code before assuming ours was equivalent. The user flow had two extra steps that ours did not, and the comments explained exactly why. That is the argument for leaving the reason in the code rather than just the mechanism: the next person to copy the pattern needs to know which parts are load-bearing.
Deleting is an edit too
Fixing the replace path exposed a third case. When a workspace is deleted, its avatar is attached — so the cleanup job, which only reaps unattached files, will never touch it. Nothing points at the file any more, and nothing ever will. It is orphaned in the one way the cleanup job is specifically unable to fix.
Deleting a workspace now releases its icon on the way out.
The general shape: if a background job decides what to keep by asking whether anything still refers to a file, then every code path that stops referring to one has to say so. Replacing. Clearing. Deleting the parent. Miss one and you get a leak; get one backwards and you get data loss. They look nothing alike from the outside, and they come from the same missing line.
What we changed
Setting a workspace avatar marks the file attached, so the cleanup job leaves it alone.
Replacing one queues the old file for deletion, into the same 30-day restore window as anything else you delete.
Deleting a workspace releases its avatar rather than stranding it.
Seven tests now cover the lifecycle, including the cases that should not fire — renaming a workspace must not queue its avatar for deletion, and re-saving the same URL is not a replacement. We checked they fail without the fix before trusting them.
Continue reading
Four ways to get it back: undo, versions, the Bin, and the write that gets refused — what undo, 30 days of version history, the Bin and the save conflict guard each recover, and where each one stops.
The assignee who no longer works here — why removing someone from a workspace clears their assignments instead of leaving a greyed-out name on forty notes.
Notez: A Note-Taking App Built for Developers — what Notez is, the ways a note gets in, and what each plan includes.