A few notes about Lace

AI Use Disclosure

The ideas here are my own. I use AI tools in various parts of the writing process, including summarizing my notes and prompts, improving grammar, tightening wording, and checking whether an argument is clear. These posts are as much notes for myself as they are for anyone else who is interested.

Lace is me taking everything I learned from developing distributed systems over the years and trying to collapse it into one thing that is actually small enough to reason about.

The original problem is this annoying distributed systems shape where two sides need to figure out, at the same time:

And then every app grows its own little matrix of push, pull, list, get, write, permissions, source policy, target policy, auth rules, cache rules, and special cases.

The configuration space explodes. You can make it work through blood, sweat, and tears, but it is a full-time job to understand the nuances of a configuration space well enough not to blow it up.

Then somebody says they want to use the system for read/write access. For example:

If you can prove to me that Bob said it is okay, I will let you read these files.

That is when you know things are about to get complicated.

The thing Lace tries to do is merge that space into one question:

which packets belong in this context?

Not “am I pushing” or “are you pulling”. Just: describe the packets that should belong to both sides, then run one exchange algorithm until the currently reachable packets are locally available and validated.

A few terms before going further:

One important distinction is that an advertisement is not truth. It is a claim about bytes I might want to request. It becomes truth only after I receive the bytes and validate them locally.

That sounds almost too simple, but it changes the shape of the problem a lot.

A small example first

Imagine Bob has four packets:

  1. a team packet sealed by Bob
  2. another team packet sealed by Bob
  3. a team packet sealed by Alice
  4. a non-team packet sealed by Bob

Bob contributes the rule:

I am willing to exchange team packets.

Alice contributes the rule:

I am looking for packets marked by Bob’s verifier, meaning packets whose Verifier field is Bob’s verifier.

The shared selected set is the intersection:

team packets marked by Bob’s verifier.

So only packets 1 and 2 move. Neither side had to write a special custom case for “team AND Verifier Bob”. Bob described his side of the packet set. Alice described her side of the packet set. Lace combined them.

That is the basic trick, and it seems almost trivial. The problem is that the example hides a load-bearing word:

Bob.

Questions that seem pretty obviously useful start showing up immediately. Like: “Who the fuck is Bob?”.

Well the best answer we have is something like: a cabal of registries sells cryptographic certificates for domain names like www.example.com, and a host-administrator will allow you to use a bob@example.com, or maybe expose some #username handle, and with some adhoc network of trust and a –small– large army of engineers we make the internet work well enough but nobody really understands it all.

Every step is the same kind of trust / authority question. Who is Bob? Who said that key is Bob? Who said Bob is in the team? Who said team membership is enough to read these packets? They dont all have the same answer, because we do want to organize differently in different situations, but they can all be expressed in the same language.

This is not a new insight, but by itself it is not super useful. All we have is the capability for one person, or one server, or one standards process, to dictate the constraints.

Best (or worst) case is we form a committee, define an RFC process, and hammer out a protocol that enough people can talk and maybe the constraints work out great and everybody is happy if a bit overworked.

The thing Lace lets us swap around is smaller and weirder than that. One person can define their constraints. Another person can define theirs. Then the system can merge those constraints over content-addressable facts.

That phrase is probably the critical bit for me right now: content-addressable facts.

A content-addressed packet is useful because I can copy it through untrusted machines and still know whether I got the same bytes. But the packet is not yet the whole story. After validation, those bytes become facts I can reason about:

packet P exists
packet P has Group = team
packet P has Bob's verifier
packet M says Bob is a member of team

A peer can advertise those claims, but they are not facts for me until I have the bytes and validate them locally. Once I do, the facts are tied back to stable packet content instead of to a server row I happened to read over a connection.

So identity, membership, publication, read/write access, routing, moderation, and whatever other distributed-system nonsense can be pushed into the same shape:

which content-addressable facts do I accept as evidence in this context?

That does not make trust go away. It just means the trust questions are ordinary rule questions over packet evidence, instead of being hidden in a dozen custom protocol phases.

But lets start at the beginning.

Packets first

The durable unit is a packet. A packet has a hash, and it can be sealed with a Verifier and Mark. That means the packet can be copied through untrusted machines without losing its byte identity. If the bytes change, the hash changes. If the Mark does not verify, validation fails.

So instead of trusting a database row because it came from a connection, Lace needs everything important to become inspectable packet facts after validation.

For example, after validation, a local store can know facts like:

So a team packet sealed by Bob might eventually produce facts like:

Have(P)
Field(P,'Group','0','team')
Field(P,'Verifier','0','V.bob...')

Peer advertisements use a separate fact world:

Advertised(P,S)
AdvertisedField(P,S,'Group','0','team')

That means source S claims to have packet P and claims it is in group team. Maybe that is true. Maybe it is a lie. Lace can use the claim to decide whether P might be worth requesting, but it must not treat the claim as a validated packet fact.

Rules without too much ceremony

A lacegram is a small rule program. You can read the rule syntax as:

the thing on the left is true when all the things on the right are true.

For example:

SelectHave(P) :- Have(P), Field(P,'Group','0','team').

Read that as:

packet P is selected from my local store when I have validated P and P has Group = team.

P is just a placeholder for a packet. The '0' is the field index; Group is a singleton field, so its index is always '0'. Most app authors should not have to write this by hand eventually; this is the lower-level rule form.

The standard exchange shape has two versions of the same human rule:

SelectHave(P) :- Have(P), Field(P,'Group','0','team').
SelectAdvertised(P,S) :- Advertised(P,S), AdvertisedField(P,S,'Group','0','team').

SelectHave looks at validated local packets. SelectAdvertised looks at peer advertisements. Same idea, different evidence worlds.

In spec language, each side contributes a selector operand, and Lace compiles those operands into a shared exchange plan. But the useful mental model is just:

my packet-set rule + your packet-set rule = the shared selected set.

Interlace

Interlace is the exchange loop. Very high level:

  1. both sides contribute their packet-set rules
  2. both sides agree on the same shared exchange plan
  3. each side advertises selected packets it already has and may send
  4. each side looks at the peer’s advertisements and requests selected packets it is missing
  5. transferred bytes are validated before becoming local facts
  6. repeat, because newly validated facts may make more packets selectable

A fixed point just means: given what both sides currently know and advertise, neither side can find another missing packet it is allowed to request from the peer.

This is the part I am most excited about. The loop naturally supports evidence closures.

Evidence as ordinary packets

Suppose I want to host chat packets for a group, but only for people who can prove they are members of that group.

A more command-shaped system might split this into phases:

  1. authenticate
  2. check membership
  3. then sync chat messages

Lace tries to avoid making that a special side channel. Membership is not a hidden pre-check. It is represented as ordinary packets and ordinary rules.

For example, another device can connect with the equivalent of:

I am a member of this group, here is my proof, and I am looking for chat packets marked by Bob’s verifier.

The rules can say:

  1. membership-proof packets belong in the selected set
  2. a verifier is an active member when I have validated their membership packet
  3. chat packets belong in the selected set when they are marked by an active member’s verifier

In rule form, that is roughly:

ActiveMember(V) :- Have(P), Field(P,'Group','0','team'), Field(P,'API','0','member'), Field(P,'Key','0',V).
SelectHave(P) :- Have(P), Field(P,'Group','0','team'), Field(P,'API','0','member').
SelectAdvertised(P,S) :- Advertised(P,S), AdvertisedField(P,S,'Group','0','team'), AdvertisedField(P,S,'API','0','member').
SelectHave(P) :- Have(P), Field(P,'Group','0','team'), Field(P,'API','0','chat'), Field(P,'Verifier','0',V), ActiveMember(V).
SelectAdvertised(P,S) :- Advertised(P,S), AdvertisedField(P,S,'Group','0','team'), AdvertisedField(P,S,'API','0','chat'), AdvertisedField(P,S,'Verifier','0',V), ActiveMember(V).

That is a dense block, but the loop is straightforward:

The system did not need a special “auth then sync” command. It just kept recomputing which packets currently belong in the selected set.

The privacy boundary

There is one important catch: peer-authored rules must not get to query my whole store.

Otherwise a peer could write a rule that says, effectively:

request my harmless bait packet only if you have some private packet I am curious about.

No private packet would be sent, but my request behavior would leak information.

So Lace has an explicit query boundary. When your rules run against my machine, they can inspect only the local packet facts I expose to you. That exposed view is local policy. If I expose nothing, your rules can still inspect your own advertisements and public exchange facts, but they cannot use my private packet store as an oracle.

That part is less cute than the packet-set story, but it is necessary. The selected-set idea only works if rule origin and query visibility are part of the algorithm.

In summary

The complicated distributed negotiation space becomes a local validation problem plus a repeated set-comparison problem.

Each side evaluates the same shared exchange plan, but with different local inputs:

Then a packet moves only when the sender currently has it and may send it, and the receiver currently sees it advertised, may request it, and is missing it.

After it moves, the receiver validates the bytes before believing anything about it. New validated packets can create new facts, which can select more packets, and the loop continues.

It is too early for me to claim it works as desired and can scale. But being able to define these requirements in the first place has simplified how hard it is to state capabilities across a distributed system.

Instead of building another protocol with separate push/pull/write/list auth paths, the question becomes: can I describe the selected packet set? If yes, interlace can try to converge the local store toward that set.

There obviously needs to be a DSL on top to make this readable. Nobody wants to hand-write a screen of Field(P,'Group','0',...) and AdvertisedField(P,S,'Group','0',...) rules for every app. But the model as a whole seems reasonably reasonable.

First written: 2026-06-18
Last edited: 2026-06-18