Field notes

Decisions · Chapter 05

A shared brain needs walls before it needs anything else

We built permissions in phase two, long before anyone was sharing anything. A family workspace without them is a different, worse product.

The Margin is meant to hold a household, not just a person. Shared boards for the things a family actually coordinates, split expenses, chores the kids can see, a calendar more than one adult writes to.

That sentence is easy to say in week two and expensive to mean.

What "family workspace" costs the moment you say it

The single-user version of this product has no hard questions in it. Everything in the database belongs to you, every query filters on one id, and the worst thing a bug can do is show you your own data twice.

Add a second person and every one of those assumptions breaks at once. Now there are things your partner should see, things your child should not, things you wrote in a shared space that you would like back, and an assistant sitting in the middle of it that has to know the difference.

None of that is a feature you add later. It is a property of every read and every write in the system, and retrofitting it means revisiting all of them.

So we built it in phase two

Before anyone was sharing anything. Before there were users to share with.

The order looked wrong at the time. We were writing a permission model for a product with one account on it, and a reasonable person could have called that premature. It is the decision I would defend hardest now.

Permissions belong to a small category of things that cannot be added to a system. They are present in its foundations or they are absent from them. A product that grows to a thousand users and then adds sharing does not get a sharing feature. It gets a migration, an audit, and a period where it is not certain what is visible to whom.

The shape we landed on

Boards are the unit. A board has members, and membership is what makes it visible, rather than the workspace it lives in or the person who created it. That distinction earns its keep, because a person belongs to several workspaces at once and their boards travel with them instead of being trapped in whichever one they were made in.

Everything else derives from it. A card is visible if its board is. A comment is visible if its card is. Sharing IS the data model rather than a second system layered over it, so there is no separate path that could quietly disagree with the first one.

Then, later, a per-item layer on top: a single note or whiteboard shared with one person, without moving it into a shared space. Households do not organise themselves cleanly, and neither does anyone else. Someone always wants to show one person one thing.

Where a shared brain gets genuinely hard

The harder problem is what the assistant is allowed to know.

If a family shares a workspace and the assistant reads everything in it, then the assistant becomes a way to learn things about the people you live with that they never told you. Nobody asked for that. Nobody would agree to it if you described it out loud.

So the memory the assistant holds is scoped the same way the data is, and there is a region scoped harder than anything else. The Vault holds content that is withheld from the assistant entirely while it is locked. No retrieval, no embedding, and no oblique mention in an answer about something else.

That last part is where the real work went, and where we have already been caught out. Hiding something from a list is easy. Making sure it cannot leak through an inferred connection, a graph edge, a hover state, or an answer synthesised from adjacent facts is a much larger claim. The Vault leaked through its own preview text once, which has a chapter of its own, and the uncomfortable part is that the leak was in a feature nobody associated with the Vault at all.

Why we state it as an absolute

Every query that touches shared content filters by the active workspace, and every one that touches board content joins through membership. Not most of them. Every one, including the ones added at eleven at night for a small feature nobody will notice.

That sounds pedantic until you watch it fail. A missing filter does not throw. It returns MORE rows, which looks like the feature working well, right up until the row belonged to somebody else.

There is no test that catches "this query returned data it should not have" unless you already knew which data that was. So the rule has to be structural, and the only version of it we trust is the one with no exceptions in it.