A Builder's Tour of Automerge
Automerge is a suite of tools for building local-first web applications with real-time synchronization that works on and offline.
In this tutorial, you'll build a local-first multiplayer app with TypeScript, React, Vite, and Automerge. You'll discover how to:
- Represent data as Automerge Documents
- Change documents' data and merge changes from different peers
- Store & synchronize a set of documents in an Automerge Repository
- Build a multiplayer realtime web app with the Automerge React client
Setup
All the code here can be found at the automerge-repo-quickstart repo.
To get started:
- clone the tutorial project from automerge-repo-quickstart
- switch to the
without-automerge
branch - in the
automerge-repo-quickstart
directory, install the project dependencies - start the local Vite development server
- npm
- yarn
- pnpm
$ git clone https://github.com/automerge/automerge-repo-quickstart
# Cloning into 'automerge-repo-quickstart'...
$ cd automerge-repo-quickstart
$ git checkout without-automerge
$ npm install
# ...installing dependencies...
$ npm run dev
$ git clone https://github.com/automerge/automerge-repo-quickstart
# Cloning into 'automerge-repo-quickstart'...
$ cd automerge-repo-quickstart
$ git checkout without-automerge
$ yarn
# ...installing dependencies...
$ yarn dev
$ git clone https://github.com/automerge/automerge-repo-quickstart
# Cloning into 'automerge-repo-quickstart'...
$ cd automerge-repo-quickstart
$ git checkout without-automerge
$ pnpm install
# ...installing dependencies...
$ pnpm dev
Visit localhost:5173/automerge-repo-quickstart/ to see the app in its "starter" state, as a basic React app not yet using Automerge: the task list can be edited, but changes are not synced between users, and all local changes are lost when the page is closed or reloaded.
Let's fix all that with Automerge!
In the exercises that follow, you'll modify the source code to:
- Configure a Repository to store & sync document changes locally
- Create/retrieve a task list Document by its Document URL
- Use the Automerge React client to update the Doc's data on user input
- Update the Repo to also sync changes over the network (when available)
Architecture of an Automerge App
Building apps with Automerge requires familiarity with two key concepts: Documents and Repositories.
- An Automerge Document (Doc) models app data using a specialized data structure that supports conflict-free collaboration via git-like merges.
- An Automerge Repository (Repo) determines how/where the app stores and synchronizes those documents, locally and/or over the network.
Automerge is built in Rust, but stack-agnostic and useful for building apps on any platform, with client libraries for many popular languages/frameworks.

The foundational Document
data structure & related algorithms are defined in the @automerge/automerge
core library, which used under the hood by the @automerge/automerge-repo
library, which exposes the practical conveniences for managing documents via a Repo
.
Manage docs with a Repo
A Repo
keeps track of all the documents you load and makes sure they're properly synchronized and stored. It provides an interface to:
- create, modify, and manage documents locally
- send & receive changes to/from others, and
- merge multiple changes as needed.
Each Repo needs to know:
- Where its documents should be saved, specified via a
StorageAdapter
- How/Where to send, retrieve, and synchronize doc updates, specified via zero or more
NetworkAdapter
s
The Repo
constructor, which comes from @automerge/automerge-repo
, lets you create & configure a Repository, specifying the StorageAdapter
and NetworkAdapter
(s) you need.
Adapters can be imported from their respective @automerge/automerge-repo-storage-*
and @automerge/automerge-repo-network-*
packages.
For convenience, we're going to use the @automerge/react
package to simplify our imports, but all that package does is re-export the most common dependencies that a React web application might want.
If none of the pre-built adapters fit your needs, you can create custom adapter(s) as needed.
Storage & Network Adapters
Currently, the task list app doesn't persist or sync any changes, even locally.
To prepare to add local multiplayer capabilities to the app, you'll initialize a local-first Repo to:
- save Docs client-side in the browser's IndexedDB, using the
IndexedDBStorageAdapter
from@automerge/automerge-repo-storage-indexeddb
- keep local users (i.e. tabs within the same browser/origin) in sync via a BroadcastChannel, using the
BroadcastChannelNetworkAdapter
.
Create a Repo to hold your documents
Start by adding @automerge/react
to your project.
- npm
- yarn
- pnpm
$ npm install @automerge/react
# ...installing dependencies...
$ yarn add @automerge/react
# ...installing dependencies...
$ pnpm add @automerge/react
# ...installing dependencies...
In src/main.tsx
, import @automerge/react
and create a Repo object configured with networking and storage.
We'll start by storing our data in IndexedDB so we don't lose it when we refresh the browser, and we'll use the inefficient but simple BroadcastChannel networking adapter to keep our browser tabs in sync.
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import {
Repo,
BroadcastChannelNetworkAdapter,
IndexedDBStorageAdapter,
} from "@automerge/react";
const repo = new Repo({
storage: new IndexedDBStorageAdapter(),
network: [new BroadcastChannelNetworkAdapter()],
});
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
There are lots of other storage and networking adapters for all kinds of different environments. We'll see more of them later.
Repos in React: RepoContext
The @automerge/react
package provides some React-specific conveniences for working with Automerge repositories.
A RepoContext
makes your repo and its documents available throughout your React application, via useRepo
and useDocument
hooks which can be called in any client component.
Add a RepoContext
to the React app
In main.tsx
, import RepoContext
and modify the React.render()
call to wrap the App
component with a RepoContext.Provider
, passing in your fresh new repo
to the context's value
prop.
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import {
Repo,
BroadcastChannelNetworkAdapter,
IndexedDBStorageAdapter,
RepoContext,
} from "@automerge/react";
const repo = new Repo({
storage: new IndexedDBStorageAdapter(),
network: [new BroadcastChannelNetworkAdapter()],
});
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<RepoContext.Provider value={repo}>
<App />
</RepoContext.Provider>
</React.StrictMode>
);
Doc Handles & URLs
A Repo
isn't very useful until it has some documents in it! To create a new document, pass its initial value to repo.create()
, which accepts a type parameter representing your data:
const listHandle = repo.create<TaskList>({
tasks: [
{
title: "Learn Automerge",
done: false,
},
],
});
The object returned from repo.create()
is a DocHandle
, which provides an interface for working with the document.
A DocHandle
's .url
property provides the document's unique identifier:
listHandle.url; // automerge:37Qr33Ub26dnS2txNCjEJDC37KFT
To retrieve a handle for a document that's already in your repo, you can pass its document URL to repo.find()
:
const existingDocHandle = repo.find(existingDoc.url);
It's common practice to pass document URLs around as URL hashes. For example:
http://my-automerge-app.com/automerge-repo-quickstart/#automerge:37Qr33Ub26dnS2txNCjEJDC37KFT
The automerge-repo
package exports an isValidAutomergeUrl()
function that you can use to determine if a given hash is a valid Document URL.
In your task list app, you'll check the page's hash and:
- retrieve the existing task list document if it exists, or
- create a new document if we don't have one already.
Quick & Dirty URL-based sharing
Now that we have automerge added to our project and a Repo available, we're going to add a simple URL-based sharing mechanism. You might recognize this approach to link sharing from other projects. Going to the page with an empty path creates a new document and puts the ID of that document into the location's hash fragment. If you open a link that includes a valid document in the hash, we open it.
This is the last bit of our setup, and while this is very convenient for prototyping and testing, it isn't the right approach for a production application. We'll discuss what we'd recommend for a production application below.
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import {
Repo,
BroadcastChannelNetworkAdapter,
WebsocketClientNetworkAdapter,
IndexedDBStorageAdapter,
RepoContext,
isValidAutomergeUrl,
} from "@automerge/react";
import type { TaskList } from "./App.tsx";
const repo = new Repo({
storage: new IndexedDBStorageAdapter(),
network: [
new WebsocketClientNetworkAdapter("wss://sync.automerge.org"),
new BroadcastChannelNetworkAdapter(),
],
});
// Check the URL for a document to load
const locationHash = document.location.hash.substring(1);
// Depending if we have an AutomergeUrl, either find or create the document
let handle;
if (isValidAutomergeUrl(locationHash)) {
handle = await repo.find(locationHash);
} else {
handle = repo.create<TaskList>({
tasks: [],
});
// Set the location hash to the new document we just made.
document.location.hash = handle.url;
}
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<RepoContext.Provider value={repo}>
<App docUrl={handle.url} />
</RepoContext.Provider>
</React.StrictMode>
);
Working with your new document
We recommend you keep Automerge documents small and granular: you can load just what you need and share with other users. As a general principle, our motto is "if the data should always travel together, put it in the same document". Remember that each document has its own history, and you can't only share part of a document.
The simplest and idomatic way of linking docs together is to use their AutomergeUrl; we'll look at an example of that later.
Docs in React: useDocument
Once you have the URL of the document you want to work with, you can access & modify it from your components with the useDocument
hook.
Similar to React's useState
, useDocument
returns a two-item array with a reactive doc
value representing the document's current contents and a changeDoc
function which can be used to update that value.
The doc
object will look and feel just like a Plain Old Javascript Object, because it is one. Just like with useState
, changes directly to the value won't behave the way you expect. Use the changeDoc
callback to update the document, recording your changes, and both saving and replicating them.
Now let's work with the document we created in the App component. We're going to use the useDocument
hook which has a similar interface to React's built-in useState
. When you build an application with Automerge, you don't have to worry about where updates come from. No matter whether they're local or remote, your application will update the same way as they arrive.
Reading a document
Let's look at reading the contents of a document. Until the document loads, it's undefined. After that, it will become a POJO.
// ...
import { useState } from "react";
import { useDocument, type AutomergeUrl } from "@automerge/react";
// ...
function App({ docUrl }: { docUrl: AutomergeUrl }) {
const [doc, changeDoc] = useDocument<TaskList>(docUrl);
// Until the document loads, useDocument returns undefined.
if (!doc) return <div>Loading...</div>;
// Now we can get the tasks out of the document and render them below.
const { tasks } = doc;
// ...
}
export default App;
Editing a document
The Automerge equivalent of setState(state => state + 1)
is changeDoc(doc => doc.state += 1)
. changeDoc
is the only way to update a document and will record any mutations you make in your callback to the doc
object.
There's one important difference between your usual JS style and working with an Automerge document: you will generally want to avoid immutable style.
It's idiomatic in JS to use syntax like spread operators to update a document, but if you do this, you'll make merging with other users ineffective. That's because Automerge doens't second-guess your intention: if you replace the whole array, we'll trust that's what you meant to do! Instead, you'll want to only update the data you actually want to change.
We've got three places we edit the document: creating a new item, toggling completion, and editing the item's text.
Here, we replace the React setState
style array spread syntax with an "unshift" call. Remember, Automerge does what you ask, so if you replace the complete array, your changes won't merge well with other users'.
<button
type="button"
onClick={() => {
changeDoc((d) =>
d.tasks.unshift({
title: "",
done: false,
})
);
}}
>
Updating the task's state is similar, but we use the index of the item to make sure we target the right item. If we weren't iterating over the array already, we could use .find()
to determine the index of the item we need.
<div className="task" key={index}>
<input
type="checkbox"
checked={done}
onChange={() =>
changeDoc((d) => {
d.tasks[index].done = !d.tasks[index].done;
})
}
/>
Updating text
Finally, we're going to handle text a little differently in this example. Following the same principle we discuss above, if you reassign a text field in an Automerge document, we will replace the whole string. This might be what you want in some cases, but often, you'll want to support collaborative editing. This can be particularly important on large documents.
There are two approaches you can use here. The simplest approach is to use the utility function updateText
. It compares the before-and-after values of a string and applies a minimum edit script to combine the two. Typically for a more advanced integration with a text editor, you would use the Automerge.splice()
function as part of an event handler, or -- ideally -- you'd just use an existing text-editor plugin like @automerge/codemirror
.
First, we'll add updateText
to our imports from the library.
import { updateText } from "@automerge/react";
Next, we replace the text updating function with one that uses it instead of just replacing the value completely.
<input
type="text"
placeholder="What needs doing?"
value={title || ""}
onChange={(e) =>
changeDoc((doc) =>
updateText(
doc, // the document to update
["tasks", 0, "title"], // array representing the path to the text to update
e.target.value
)
)
}
style={done ? { textDecoration: "line-through" } : {}}
/>
Collaboration in Automerge
As the name implies, one of the key powers of Automerge is its ability to merge different changes to a given document, much like git lets you merge multiple edits to a given file.
When merging different changes to the same property, Automerge uses various strategies to avoid conflicts and ensure that the merged document will be identical for every user.
This makes Automerge an ideal tool for building collaborative apps that let multiple users work together on the same documents (with or without a network connection).
Collaborating Locally
Since the Repo in this app uses a BroadcastChannelNetworkAdapter
, any changes made to documents in that Repo sync automatically to all other clients with the same origin (i.e. tabs within the same browser) who know the given document's URL (its unique identifier).
Open a second tab with the same URL and edit the list, and you'll see the first tab's list updated accordingly. If you close all the tabs and reopen them, the document is preserved, as it is stored in your browser's IndexedDB.
That's right, you've already built a working Automerge-backed React app with live local synchronization! Congrats!
Collaborating over the internet
Thus far, we've been using the BroadcastChannel NetworkAdapter to move data between tabs in the same browser. Automerge treats all network adapters similarly: they are just peers you may choose to synchronize documents with.
One straightforward way of getting data to other people is to send it to the cloud; then they can come along and fetch the data at their leisure.
When you configure automerge to run on an internet server, listen for connections, and store data on disk, then we call that a "sync server". There's nothing really special about a sync server: it runs the exact same version of Auotmerge as you run locally. With a little configuration work, you could even connect to multiple sync servers and choose what data you want to send.
The Automerge team provides a public community sync server at wss://sync.automerge.org
. For production software, you should run your own server, but for prototyping and development you are welcome to use ours on an "as-is" basis.
Connect to a sync server via a websocket
This is as simple as adding WebSocketClientAdapter
to your Repo's network subsystem. We'll do this at creation time, but remember you can add and remove adapters later, too.
Solution
//...
import { WebSocketClientAdapter } from "@automerge/react";
const repo = new Repo({
network: [
new BroadcastChannelNetworkAdapter(),
new WebSocketClientAdapter("wss://sync.automerge.org"),
],
storage: new IndexedDBStorageAdapter(),
});
Now, when the Repo sees any changes it will sync them not only locally via the BroadcastChannel, but also over a websocket connection to sync.automerge.org
, and any other process can connect to that server and use the URL to get the changes we've made.
The Automerge project provides a public sync server for you to experiment with, at sync.automerge.org
. This is not a private instance, and as an experimental service has no reliability or data safety guarantees. Feel free to use it for demos and prototyping, but run your own sync server for production apps.
To see this in action, open the same URL (including the document ID) in a different browser, or on a different device. Unlike the local-only version, you'll now see the data updates synced across all open clients.
Network Not Required
Now that the Repo is syncing changes remotely, what happens when the websocket connection is unavailable?
Since the repo stores documents locally with the IndexedDBStorageAdapter
, methods like Repo.find
will consult local storage to retrieve/modify documents, so clients can create new documents while disconnected, and any clients who've already loaded a given document will still be able to make changes to it while offline.
Once connectivity has been re-established, the Repo will sync any local changes with those from remote peers, so everyone ultimately sees the same data.
Go ahead and experiment with this by opening your site in two browsers, turning off wifi, making some changes, and turning it back on.
Next Steps
Congratulations! You've built a local-first, offline-capable app that supports multiplayer collaboration locally and over the network.
If you're hungry for more: