Skip to content

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
PackageContents
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.

TypeConcurrent edits resolve asReach for it when
BcrdtTextBoth edits survive, each author's run stays contiguousAnything a person types into at length
BcrdtArrayBoth insertions survive, in a stable orderLists, ordered collections
BcrdtMapOne value wins, deterministically but arbitrarilyFields where a later value legitimately replaces an earlier one
BcrdtSetAn add wins over a concurrent remove - if it added somethingTags, 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.
An unhandled error has occurred. Reload 🗙