Live collaboration,
merged in C#.
A CRDT engine for text, lists, maps and sets, with the synchronization, presence, offline-first persistence and selective undo a collaborative feature actually needs - and no Yjs, no JavaScript CRDT library, no interop in the engine. Two people editing the same document end up with the same document, whether they were online at the time or not.
dotnet add package Bit.Bcrdt
<BcrdtCollaborationScope DocumentId="notes-42" User="_me" Context="document"> <BcrdtTextArea Text="@document.Text("body")" Presence="document.Presence" /> </BcrdtCollaborationScope>
Opening two replicas…
Type into either one. Then take one offline, type into both, and bring it back.
Capabilities
Everything a collaborative feature needs
The hard part of collaboration is never the UI: it is two edits crossing in flight, a connection dropping mid-sentence, a client coming back after a week offline. Every page of this site is a set of real, separate replicas, so the merges you watch are real merges.
Collaborative text
BcrdtTextArea turns every keystroke into the minimal insertion or deletion and keeps
your caret in place while somebody types above it.
Nested types
A BcrdtArray of BcrdtMaps: a rename and a concurrent tick of the same
item touch different keys, so both survive.
A merge you choose
A map entry is last-writer-wins, a nested text merges character by character - the field type you pick decides what happens when two people edit it at once.
Collaborative form →Conflicts, on purpose
Concurrent insertion at the same position, deletion of the same element, writes to the same key - each provoked, with its outcome stated before it runs.
Conflict lab →Selective undo
BcrdtUndoManager reverses your own edits and leaves everyone else's alone - and the
undo itself synchronizes like any other edit.
Live, across tabs
One replica per browser tab, through a SignalR hub or a WebSocket endpoint that only relays - no copy of the document lives on the server.
Live, across tabs →In practice
See it in code
Register it once
One registration configures the transport and the store, in every project that renders collaborative components. Each seam is optional: with none of them you still get a working, purely local document.
Getting started →builder.Services.AddBitBcrdtServices(collaboration => collaboration
.UseTransport(new BcrdtSignalRTransportFactory(hubUrl))
.UseInMemoryStore());Map a relay that only relays
BcrdtHub forwards each message to the other clients of a document without
decoding it, and holds no copy of it. The WebSocket endpoint does the same for the built-in
WebSocket transport.
builder.Services.AddBitBcrdtHub(); // ... app.MapHub<BcrdtHub>("/hub/bcrdt");
Open a document and edit it
Opening loads from storage first, so the document is usable at once and offline, then connects and lets the handshake reconcile whatever changed meanwhile. A transaction is one update and one round of events.
Internals →var document = await collaboration.OpenAsync("design-review"); using (var transaction = document.BeginTransaction()) { document.Map.Set("title", "Q3 planning"); document.Array("attendees").Push("Ada", "Grace"); document.Text("notes").Insert(0, "Agenda:\n"); }
Comparison
Why a CRDT?
Without one you have to pick a loser: lock the document, funnel every keystroke through a server that rewrites everyone's edits into one agreed order, or save last and overwrite somebody's work. A CRDT is built so that applying edits in any order, more than once, or after any delay gives the same result on every copy.
| Concern | Last write wins | Operational transformation | Bcrdt |
|---|---|---|---|
| Two people typing at once | One of them loses their work | Both survive, ordered by the server | Both survive, in the same order on every replica |
| Editing offline | Overwrites whatever changed meanwhile | Not possible - every edit needs the server | A replica that has not heard anything for a while |
| Central authority | The store - the last write to reach it wins | An ordering server, for every keystroke | None - a relay only forwards bytes |
| What goes over the wire | The whole record | Every operation, transformed | One small delta per transaction |
| A message delivered twice | A late duplicate can overwrite a newer value | Has to be prevented | Harmless - applying it again changes nothing |
Where to start
Choose your path
New to Bcrdt?
Install, register, map the relay - a collaborative editor in five minutes.
Getting started →Need to see it to believe it?
Provoke the conflicts that break naïve synchronization, and compare both replicas' raw state.
Open the conflict lab →Building with an AI agent?
This site is an MCP server: point your agent at /mcp and it runs the real engine instead of guessing what a merge decides.
Questions
Frequently asked
Is this Yjs running through JavaScript interop?
No. The engine, the wire format and the merge algorithm are C#. The only JavaScript is a small module
that reads and sets the caret of a <textarea>, because the browser does not expose
one to .NET - and nothing in the engine calls it.
What happens when two people change the same field at once?
It depends on the type you chose. A BcrdtMap key keeps one of the two values -
deterministically, the same one everywhere, but arbitrarily. A nested BcrdtText keeps
both edits. Where losing a write is unacceptable, model the value as a nested collaborative type.
Does it need a server?
Not to merge. Replicas need a way to exchange messages, and the relays in Bit.Bcrdt.Server
forward them without decoding one or holding a replica. Replicas in one process - as on every page of
this site but the live one - talk over the in-memory transport and need nothing at all.
What happens while a replica is offline?
Nothing special. Local edits accumulate in the document, the sync provider reconnects with jittered backoff, and the handshake sends each side what the other is missing - in both directions. There is no outbound queue: the document is the queue, and it compacts itself.
Which render modes does it work in?
All of them - WebAssembly, Server, Web App and Hybrid, on .NET 8, 9 and 10. The service is registered scoped, so on Blazor Server every circuit gets its own replica and its own client identity.
Let them all type at once
One registration, one relay, one component - and nobody's work is overwritten again.
dotnet add package Bit.Bcrdt