Getting started
One package for the engine and the Blazor components, one for the transport, one for the relay - then a
BcrdtTextArea inside a BcrdtCollaborationScope is a working collaborative editor
with live presence, automatic reconnection and offline support.
Install
Bit.Bcrdt carries the engine, the synchronization, presence and persistence, the in-memory and
WebSocket transports and the Blazor components. The SignalR client transport is a package of its own with no
ASP.NET Core server dependency, so it runs in WebAssembly and Hybrid apps too.
# the engine and the Blazor components dotnet add package Bit.Bcrdt # the SignalR transport - or use the WebSocket transport built into Bit.Bcrdt, or none dotnet add package Bit.Bcrdt.SignalR
| Package | Contents |
|---|---|
| Bit.Bcrdt | The engine - BcrdtText, BcrdtArray, BcrdtMap and BcrdtSet, updates, snapshots and selective undo - with presence, the stores, the sync protocol, AddBitBcrdtServices, the input bindings, the in-memory and WebSocket transports and the Blazor components. |
| Bit.Bcrdt.SignalR | The SignalR client transport. |
| Bit.Bcrdt.Server | The ASP.NET Core relays: BcrdtHub for the SignalR transport and MapBitBcrdtWebSocket for the WebSocket transport. |
| Bit.Bcrdt.Testing | A deterministic, seeded network simulator, random operation generators, a convergence harness and an update corrupter for fuzzing. |
Register the services
Register it once, in every project that renders collaborative components - in a Blazor Web App, that is both the server and the client project. The .NET SignalR client needs an absolute hub URL, so resolve it against the host; in Blazor WebAssembly that is the host page's base address.
using Bit.Bcrdt; using Bit.Bcrdt.SignalR; string hubUrl = new Uri(new Uri(builder.HostEnvironment.BaseAddress), "hub/bcrdt").ToString(); builder.Services.AddBitBcrdtServices(collaboration => collaboration .UseTransport(new BcrdtSignalRTransportFactory(hubUrl)) .UseInMemoryStore());
AddBitBcrdtSignalRTransport(hubUrl) is the same registration as a one-liner. Each of the three
seams - transport, store and options - is optional: an application that configures none of them still gets a
working, purely local collaborative document, so a feature can be built and tested without a server and then
connected to one without changing how it reads or writes state.
Map the relay
The hub lives in a separate server-side package, so that the client transport carries no ASP.NET Core server
dependency. Use AddBitBcrdtHub rather than a bare AddSignalR: SignalR's default
32 KB receive limit is smaller than the handshake reply for any document of real size.
dotnet add package Bit.Bcrdt.Server # in the server project
using Bit.Bcrdt.Server; builder.Services.AddBitBcrdtHub(); // AddSignalR, with a receive limit sized for sync messages // ... app.MapHub<BcrdtHub>("/hub/bcrdt");
The raw WebSocket transport has its relay in the same package: call app.UseWebSockets() and map
it with app.MapBitBcrdtWebSocket("/ws/bcrdt/{documentId}"). Neither relay decodes a message or
holds a replica - they only forward bytes between the clients of a document.
A first editor
BcrdtCollaborationScope opens the document, renders its content with the document as context,
shows LoadingContent while it opens and releases the document when it goes away.
BcrdtTextArea turns every keystroke into the minimal edit and publishes the caret through
presence. There is no stylesheet to reference and no script tag to add: the one small module the textarea
needs is imported on demand.
@using Bit.Bcrdt @using Bit.Bcrdt.Blazor <BcrdtCollaborationScope DocumentId="notes-42" User="_me" Context="document"> <BcrdtPresenceView Presence="document.Presence" /> <BcrdtTextArea Text="@document.Text("body")" Presence="document.Presence" /> </BcrdtCollaborationScope> @code { private readonly BcrdtCollaboratorInfo _me = new("u-1", "Ada", BcrdtPresenceService.SuggestColor("u-1")); }
In a page that is prerendered and then becomes interactive, open documents only once the component is
interactive (RendererInfo.IsInteractive) - a document opened while prerendering belongs to a
request that is already over. The live page is exactly this, across browser tabs.
Choose the data type
This is the decision that matters most: the merge is decided by the data structure rather than by your application, so it is worth knowing what each one will decide.
| Type | Concurrent edits resolve as | Reach for it when |
|---|---|---|
| BcrdtText | Both edits survive, each author's run stays contiguous | Anything a person types into at length |
| BcrdtArray | Both insertions survive, in a stable order | Lists, ordered collections |
| BcrdtMap | One value wins, deterministically but arbitrarily | Fields where a later value legitimately replaces an earlier one |
| BcrdtSet | An add wins over a concurrent remove - if it added something | Tags, membership, flags |
The trap is a BcrdtMap with a long string in it: two people editing that field concurrently
means one of them loses everything they typed. If a field is something a human types prose into, make it a
nested BcrdtText and both edits merge - the collaborative form shows the two
side by side.
document.Map.EnsureText("description").Insert(0, "..."); // merges document.Map.Set("description", "..."); // one side wins
Where to go next
- Collaborative text - three replicas of one paragraph, and what happens when one goes offline.
- Conflict lab - six conflicts provoked on purpose, each with its outcome stated before it runs.
- Collaborative undo - a stack per replica that undoes only its own edits.
- MCP server - point an AI agent at this site and it reads the real API, the guide and these pages.
- The full reference guide - persistence, presence, transports, the architecture and the correctness tests - is the README on GitHub.