Notez workspaces have held more than one person for a while now. Notes have had typed, filterable properties for longer. The obvious thing sitting in the gap between those two features is an assignee — a note that says this one is Marta's.
That is a small feature. A dropdown of the people in the workspace, an id in a column, a chip on the card. An afternoon of work.
The part that is not small is the question nobody asks until it happens: Marta leaves. What happens to the forty notes with her name on them?
Member is a property, not a field
The first decision was the boring one, and it was boring on purpose.
We could have added an assigneeId column to notes and built a picker for it. Instead, member became an eleventh custom property type, sitting alongside text, number, date, select, relation, and the rest.
The difference shows up everywhere except the code that stores it. Because member is a property:
You can have more than one of them.
AssigneeandReviewerare two properties, not a schema change.It appears in the filter popover, the sort menu, and the Table view column picker without any of those learning what a person is.
It shows as a chip on cards, in table cells, and on the note itself, through the same rendering path as every other property.
The whole feature is one new case in about a dozen switch statements, each of which refuses to compile until the case is handled. The compiler wrote the checklist.
The precedent was already there: relation, the type that points at other notes. Multi-valued, ordered, foreign-keyed to the thing it references, resolved server-side so the chip and the filter can never disagree about what a value means. A member property is a relation property whose target is a person instead of a note. We copied it almost line for line.
Where the id lives
A member value is a user id. There was a tempting shortcut: property values already have a valueText column, and a user id is text. Store it there, ship a day earlier.
We gave it its own column with a real foreign key instead, for one reason that is not tidiness.
Accounts get deleted. When somebody deletes their Notez account, that row goes, and every reference to it has to go with it — otherwise you are left with a value pointing at a user that no longer exists, and every read has to defensively filter for ghosts. With a foreign key and on delete cascade, the database does that itself, correctly, forever, in a single line of schema. With valueText, we would have owned it: a manual cleanup in the account-deletion hook, plus a dangling-id filter on every read path, plus the bug we eventually ship when somebody adds a twelfth read path and forgets.
The check constraint that says exactly one payload column is non-null grew from five columns to six. That is the whole cost.
The actual question
Now the interesting one. Marta is removed from the workspace, or leaves. Her name is on forty notes. There are exactly three things you can do:
Block the removal. Refuse to remove somebody until their assignments are cleared. This is the worst option and it takes about ten seconds to see why: Marta cannot leave a workspace she wants out of until an owner hand-edits forty notes. A safety mechanism that holds a person hostage is not a safety mechanism.
Keep the value and mark it dead. Leave the row, resolve the name anyway, render it greyed out — a tombstone. This is what we do for relation values pointing at trashed notes, so it had precedent and it was our first plan.
Delete the values with the membership. Removing somebody clears their assignments in the same transaction.
We went with the third, and reversed the tombstone plan while writing it down.
Why tombstones lose here
Tombstones are right for a trashed note. The note still exists, in the Bin, restorable for thirty days. The tombstone is pointing at something, and it is telling you the truth: this target is in the bin.
A departed member is not in a bin. There is nothing to restore. And two specific things go wrong:
Nobody can ever find those notes again. The member filter lists the people currently in the workspace — that is what makes it a useful picker rather than an ever-growing list of everyone who has ever touched the workspace. So Marta is not in it. Forty notes now carry a value that no filter can select for. They are not findable, only stumble-upon-able.
The notes become quietly hostile to edit. Every save of a multi-value property sends the whole array. Write validation checks that every id is a current member. So a note assigned to Marta and to somebody still here would reject the next save — you would have to notice Marta's dead chip and remove it before you could add anyone. We could carve out an exception for ids that are already stored, and that carve-out is exactly the kind of subtle rule that turns into a bug two features from now.
Delete has a real cost and it is worth stating plainly: re-inviting somebody does not bring their assignments back. That is genuinely lost information. We think it is the better trade, because the alternative is information that is present but unreachable, which is worse than absent — it looks like data and behaves like litter.
The precedent for deleting turned out to be right there in the same file. Deleting a select option already cascades its values off every note. The confirmation dialog for removing a member already says they will lose access to every note in this workspace. Clearing the assignment is consistent with both.
The one that got away
There is a fourth scenario the transaction does not cover, and it is worth being honest about it.
A member removed between the validation check and the insert — a window of milliseconds — leaves a row the cleanup never saw. The user still exists, so the chip still renders. It is a stale value, and the next edit of that property rejects until you untick it.
We could close it with a lock. We chose not to: the window is milliseconds, the failure is one confusing chip rather than lost data, and the pinned-selected-row behaviour in the picker means you can always untick it. Some races are worth a comment rather than a lock.
What is deliberately missing
Assigning from AI chat. When this shipped, chat could read who is on a note but not set one: no tool handed it the list of people in your workspace, so it could only guess at an id, and a plausible-looking guess is the worst possible failure mode for a write. Chat now gets every member of the workspace with their id and sets a member property like any other value.
Boards grouped by assignee. The board is wired to its own column property. Grouping by an arbitrary property is a bigger change than this one.
"Assigned to me." A one-click filter for your own name was the obvious next thing, and it has since shipped: switch on Show in sidebar for a member property, and the sidebar lists everyone in the workspace with a count of their notes. One click on your name filters to yours.
Notifications. Being assigned does not tell you. Tell your colleague.
When this was written, two of those four were hard and two were just not built yet. Assigning from chat and the filter have shipped since; boards grouped by assignee and notifications have not. The difference matters, and we would rather say which is which than let a feature list imply everything is imminent.
The shape of the lesson
Adding a field is easy. What makes a field a feature is deciding what it does on the days that are not the happy path — when the person leaves, when the account is deleted, when two writes race.
We spent maybe a fifth of the time on the picker and the chip. Most of it went on the paragraph in the help article that explains what happens when somebody leaves — because writing that sentence down is what proved the first plan was wrong.
Continue reading
Sharing without the merge conflict — why a shared workspace refuses the second of two conflicting saves instead of merging them.
Two assistants, one toolset — how AI chat and the MCP server drifted to sixteen and ten tools, and the note-tree bug that turned up while matching them.
Notez: A Note-Taking App Built for Developers — what Notez is, the ways a note gets in, and what each plan includes.