P2P WebRTC Replication with RxDBSync Data between Browsers and Devices in JavaScript
WebRTC P2P data connections are revolutionizing real-time web and mobile development by eliminating central servers in scenarios where clients can communicate directly. With the RxDB Sync Engine, you can sync your local database state across multiple browsers or devices via WebRTC P2P (Peer-to-Peer) connections, ensuring scalable, secure, and low-latency data flows without traditional server bottlenecks.
What is WebRTC?
WebRTC stands for Web Real-Time Communication. It is an open standard that enables browsers and native apps to exchange audio, video, or arbitrary data directly between peers, bypassing a central server after the initial connection is established. WebRTC uses NAT traversal techniques like ICE (Interactive Connectivity Establishment) to punch through firewalls and establish direct links. This peer-to-peer nature drastically reduces latency while maintaining high security and end-to-end encryption capabilities.
For a deeper look at comparing WebRTC with WebSockets and WebTransport, you can read our comprehensive overview. While WebSockets or WebTransport often work in client-server contexts, WebRTC offers direct peer-to-peer connections ideal for fully decentralized data flows.
Benefits of P2P Sync with WebRTC Compared to Client-Server Architecture
- Reduced Latency - By skipping a central server hop, data travels directly from one client to another, minimizing round-trip times and improving responsiveness.
- Scalability - New peers can join without overloading a central infrastructure. The sync overhead increases linearly with the number of connections rather than requiring a massive server cluster.
- Privacy & Ownership - Data stays within the user’s devices, avoiding risks tied to storing data on third-party servers. This design aligns well with local-first or "zero-latency" apps.
- Resilience - In some scenarios, if the central server is unreachable, P2P connections remain operational (assuming a functioning signaling path). Apps can still replicate data among local networks like when they are in the same Wifi or LAN. Notice that the peers still have to find each other through a signaling server, so this is not a full offline mesh. For DDIL environments without any infrastructure at all, compare the transports in the Ditto alternative page.
- Cost Savings - Reducing the reliance on a high-bandwidth server can cut hosting and bandwidth expenses, particularly in high-traffic or IoT-style use cases.
Peer-to-Peer (P2P) WebRTC Replication with the RxDB JavaScript Database
Traditionally, real-time data synchronization depends on centralized servers to manage and distribute updates. In contrast, RxDB’s WebRTC P2P replication allows data to flow directly among clients, removing the server as a data store. This approach is live and fully decentralized, requiring only a signaling server for initial discovery:
- No master-slave concept - each peer hosts its own local RxDB.
- Clients (browsers, devices) connect to each other via WebRTC data channels.
- The RxDB replication protocol then handles pushing/pulling document changes across peers.
Because RxDB is a NoSQL database and the replication protocol is straightforward, setting up robust P2P sync is far easier than orchestrating a complex client-server database architecture.
Using RxDB with the WebRTC Replication Plugin
Before you use this plugin, make sure that you understand how WebRTC works. Here we build a todo-app that replicates todo-entries between clients:
You can find a fully build example of this at the RxDB Quickstart Repository which you can also try out online.
First you create the database and then you can configure the replication:
Create the Database and Collection
Here we create a database with the localstorage based storage that stores data inside of the LocalStorage API in a browser. RxDB has a wide range of storages for other JavaScript runtimes.
import { createRxDatabase } from 'rxdb/plugins/core';
import { getRxStorageLocalstorage } from 'rxdb/plugins/storage-localstorage';
const db = await createRxDatabase({
name: 'myTodoDB',
storage: getRxStorageLocalstorage()
});
await db.addCollections({
todos: {
schema: {
title: 'todo schema',
version: 0,
type: 'object',
primaryKey: 'id',
properties: {
id: { type: 'string', maxLength: 100 },
title: { type: 'string' },
done: { type: 'boolean', default: false },
created: { type: 'string', format: 'date-time' }
},
required: ['id', 'title', 'done']
}
}
});
// insert an example document
await db.todos.insert({
id: 'todo-1',
title: 'P2P demo task',
done: false,
created: new Date().toISOString()
});Import the WebRTC replication plugin
import {
replicateWebRTC,
getConnectionHandlerSimplePeer
} from 'rxdb/plugins/replication-webrtc';Start the P2P replication
To start the replication you have to call replicateWebRTC on the collection.
As options you have to provide a topic and a connection handler function that implements the P2PConnectionHandlerCreator interface. As default you should start with the getConnectionHandlerSimplePeer method which uses the simple-peer library and comes shipped with RxDB.
const replicationPool = await replicateWebRTC(
{
// Start the replication for a single collection
collection: db.todos,
// The topic is like a 'room-name'. All clients with the same topic
// will replicate with each other. In most cases you want to use
// a different topic string per user. Also you should prefix the topic with
// a unique identifier for your app, to ensure
// you do not let your users connect
// with other apps that also use the RxDB P2P Replication.
topic: 'my-users-pool',
/**
* You need a collection handler to be able to create WebRTC connections.
* Here we use the simple peer handler which
* uses the 'simple-peer' npm library.
* To learn how to create a custom connection handler, read the source code,
* it is pretty simple.
*/
connectionHandlerCreator: getConnectionHandlerSimplePeer({
// Set the signaling server url.
// You can use the server provided by RxDB for tryouts,
// but in production you should use your own server instead.
signalingServerUrl: 'wss://signaling.rxdb.info/',
// only in Node.js, we need the wrtc library
// because Node.js does not have WebRTC.
// Wrap with createSimplePeerWrtc().
wrtc: createSimplePeerWrtc(
require('node-datachannel/polyfill')
),
// only in Node.js, we need the WebSocket library
// because Node.js does not contain the WebSocket API.
webSocketConstructor: require('ws').WebSocket
}),
pull: {},
push: {}
}
);Notice that in difference to the other replication plugins, the WebRTC replication returns a replicationPool instead of a single RxReplicationState. The replicationPool contains all replication states of the connected peers in the P2P network.
Observe Errors
To ensure we log out potential errors, observe the error$ observable of the pool.
replicationPool.error$.subscribe(err => console.error('WebRTC Error:', err));Live replications
The WebRTC replication is always live because there can not be a one-time sync when it is always possible to have new Peers that join the connection pool. Therefore you cannot set the live: false option like in the other replication plugins.
Signaling Server
For P2P replication to work with the RxDB WebRTC Replication Plugin, a signaling server is required. The signaling server helps peers discover each other and establish connections.
RxDB ships with a default signaling server that can be used with the simple-peer connection handler. This server is made for demonstration purposes and tryouts. It is not reliable and might be offline at any time. In production you must always use your own signaling server instead!
Creating a basic signaling server is straightforward. The provided example uses 'socket.io' for WebSocket communication. However, in production, you'd want to create a more robust signaling server with authentication and additional logic to suit your application's needs.
Here is a quick example implementation of a signaling server that can be used with the connection handler from getConnectionHandlerSimplePeer():
import {
startSignalingServerSimplePeer
} from 'rxdb/plugins/replication-webrtc';
const serverState = await startSignalingServerSimplePeer({
port: 8080 // <- port
});For custom signaling servers with more complex logic, you can check the source code of the default one.
Signaling over Nostr Relays
Instead of running your own signaling server, you can let the peers find each other over Nostr relays. Nostr is an open protocol where clients publish signed JSON events to relays, which are plain WebSocket servers. There are many public relays, and you can run your own with open source software like strfry or nostr-rs-relay.
The connection handler from getConnectionHandlerNostr() sends the WebRTC offers, answers, and ICE candidates as Nostr events. After the peers are connected, the replicated documents go directly over the WebRTC data channel and never touch a relay.
import {
replicateWebRTC,
getConnectionHandlerNostr
} from 'rxdb/plugins/replication-webrtc';
const replicationPool = await replicateWebRTC(
{
collection: myRxCollection,
topic: 'my-users-pool',
connectionHandlerCreator: getConnectionHandlerNostr({
// Public relays, all peers of a topic must share at least one.
relays: [
'wss://nos.lol',
'wss://relay.primal.net',
'wss://nostr.mom'
],
/**
* (optional) Nostr secret key (32 bytes) of this peer.
* If not set, a random key is generated on each start.
*/
// secretKey: mySecretKey,
/**
* (optional) kind of the Nostr events that are used for signaling.
* Must be an ephemeral kind between 20000 and 29999.
* [default=25050]
*/
// eventKind: 25050
}),
pull: {},
push: {}
}
);How the signaling works:
- Ephemeral events: All signaling events use an ephemeral kind, so relays forward them to the subscribers but do not store them.
- Hashed topic: The events are tagged with a SHA-256 hash of the
topic, so the topic name is not readable on the relays. - Presence: Each peer publishes a presence event every
10s. A peer that sends no presence for35sis removed from the room. - Encrypted signals: Offers, answers, and ICE candidates are encrypted with NIP-44 to the public key of the receiving peer. Only the receiving peer can read them.
- Signed events: Every event is signed with the key of the sending peer, and events with an invalid signature are ignored. Because the signals that set up the WebRTC connection are signed, the public key of a connected peer is verified.
- Batched signals: Many public relays rate limit how many events a client can send per second. Trickle ICE creates many signals at once, so the signals to the same peer are collected for
200msand sent together in one event. - Multiple relays: The handler connects to all given relays and reconnects each of them with the same backoff as the signaling server connection. Events that arrive from more than one relay are only processed once.
The same secret key can be used on multiple devices or browser tabs at the same time, because each connection handler adds its own random session id to the peer id.
The public relays wss://nos.lol, wss://relay.primal.net, and wss://nostr.mom from the example above were tested with this handler on September 30, 2026. Public relays are run by third parties and can be offline, change their policies, or rate limit your app at any time. For production, run your own relay or pick relays that you trust, and pass more than one relay url.
Peer Validation
By default the replication will replicate with every peer the signaling server tells them about.
You can prevent invalid peers from replication by passing a custom isPeerValid() function that either returns true on valid peers and false on invalid peers.
const replicationPool = await replicateWebRTC(
{
/* ... */
isPeerValid: async (peer) => {
return true;
}
pull: {},
push: {}
/* ... */
}
);With the Nostr connection handler, each peer has a verified Nostr public key. You can use it to only replicate with known peers. Give each device its own secretKey and store the public keys of the allowed devices:
import {
replicateWebRTC,
getConnectionHandlerNostr,
getNostrPublicKeyOfPeer
} from 'rxdb/plugins/replication-webrtc';
import { getPublicKey } from 'nostr-tools/pure';
// hex public keys of the devices that are allowed to replicate
const allowedPublicKeys = [
getPublicKey(mySecretKey),
publicKeyOfOtherDevice
];
const replicationPool = await replicateWebRTC(
{
collection: myRxCollection,
topic: 'my-users-pool',
connectionHandlerCreator: getConnectionHandlerNostr({
relays: ['wss://nos.lol', 'wss://relay.primal.net'],
secretKey: mySecretKey
}),
isPeerValid: (peer) => {
const publicKey = getNostrPublicKeyOfPeer(peer);
return allowedPublicKeys.includes(publicKey);
},
pull: {},
push: {}
}
);Connection Handling and Timeouts
The simple-peer connection handler reconnects on its own when the connection to the signaling server or to another peer breaks. Reconnects run with an exponential backoff that starts at 500ms and is capped at 15s, so that an unreachable signaling server does not cause a busy loop. A peer connection that is not established within 15s is dropped and a new attempt is started. Existing WebRTC connections keep replicating while the signaling server is offline.
Messages that are bigger than the message size limit of the WebRTC data channel (which can be as low as 64 KiB depending on the browser) are split into chunks, so you can replicate big documents.
Each request to another peer fails when no answer arrives in time. The replication then retries after retryTime. You can change the timeout with the requestTimeout option:
const replicationPool = await replicateWebRTC(
{
/* ... */
// (optional) time in milliseconds [default=20000]
requestTimeout: 30000,
pull: {},
push: {}
}
);Conflict detection in WebRTC replication
RxDB's conflict handling works by detecting and resolving conflicts that may arise when multiple clients in a decentralized database system attempt to modify the same data concurrently. A custom conflict handler can be set up, which is a plain JavaScript function. The conflict handler is run on each replicated document write and resolves the conflict if required. Find out more about RxDB conflict handling here
Known problems
SimplePeer requires to have process.nextTick()
In the browser you might not have a process variable or process.nextTick() method. But the simple peer uses that so you have to polyfill it.
In webpack you can use the process/browser package to polyfill it:
const plugins = [
/* ... */
new webpack.ProvidePlugin({
process: 'process/browser',
})
/* ... */
];In angular or other libraries you can add the polyfill manually:
window.process = {
nextTick: (fn, ...args) => setTimeout(() => fn(...args)),
};
Polyfill the WebSocket and WebRTC API in Node.js
While all modern browsers support the WebRTC and WebSocket APIs, they is missing in Node.js which will throw the error No WebRTC support: Specify opts.wrtc option in this environment. Therefore you have to polyfill it with a compatible WebRTC and WebSocket polyfill. It is recommended to use the node-datachannel package for WebRTC which does not come with RxDB but has to be installed before via npm install node-datachannel --save.
For the Websocket API use the ws package that is included into RxDB.
Because the node-datachannel/polyfill has read-only properties on RTCSessionDescription that are incompatible with simple-peer, you must use the createSimplePeerWrtc() wrapper:
import nodeDatachannelPolyfill from 'node-datachannel/polyfill';
import { WebSocket } from 'ws';
import { createSimplePeerWrtc } from 'rxdb/plugins/replication-webrtc';
const replicationPool = await replicateWebRTC(
{
/* ... */
connectionHandlerCreator: getConnectionHandlerSimplePeer({
signalingServerUrl: 'wss://example.com:8080',
wrtc: createSimplePeerWrtc(nodeDatachannelPolyfill),
webSocketConstructor: WebSocket
}),
pull: {},
push: {}
/* ... */
}
);Storing replicated data encrypted on client device
Storing replicated data encrypted on client devices using the RxDB Encryption Plugin is a pivotal step towards bolstering data security and user privacy. The WebRTC replication plugin seamlessly integrates with the RxDB encryption plugins, providing a robust solution for encrypting sensitive information before it's stored locally. By doing so, it ensures that even if unauthorized access to the device occurs, the data remains protected and unintelligible without the encryption key (or password). This approach is particularly vital in scenarios where user-generated content or confidential data is replicated across devices, as it empowers users with control over their own data while adhering to stringent security standards. Read more about the encryption plugins here.
FAQ
How can WebRTC enable real-time peer-to-peer communications between browsers?
WebRTC enables true peer-to-peer (P2P) communication by establishing direct UDP/TCP data channels between browsers, completely bypassing centralized database architectures. Because the WebRTC connection requires initial IP discovery, clients must briefly connect to a WebSocket signaling server or to Nostr relays to exchange SDP offers and ICE candidates. Once peered, the RxDB WebRTC Replication plugin streams NoSQL document diffs and CRDT operations instantly across the channel, providing decentralized real-time sync with absolute zero cloud latency.
Which distributed database services offer peer discovery and sync plugins?
RxDB offers comprehensive peer discovery and sync plugins for distributed applications. The WebRTC replication plugin facilitates direct peer-to-peer data synchronization. A signaling server or a set of Nostr relays handles initial peer discovery and connection establishment. You connect browsers and mobile apps without a central database server. The sync engine automatically replicates local changes across all discovered peers.
What are the top databases that sync directly between devices without cloud dependency?
Very few databases support true decentralized peer-to-peer (P2P) synchronization. RxDB is one of the leading options for this architecture, offering a dedicated WebRTC replication plugin that allows direct, client-to-client data synchronization via WebRTC data channels without routing through a central cloud database. Other notable decentralized tools include Ditto, GunDB, and CRDT-based libraries like Yjs or Automerge (though these are often data structure libraries, not fully queryable databases).
Follow Up
- Check out the RxDB Quickstart to see how to set up your first RxDB database.
- Explore advanced features like Custom Conflict Handling or Offline-First Performance.
- Try an example at RxDB Quickstart GitHub to see a working P2P Sync setup.
- Join the RxDB Community on GitHub or Discord if you have questions or want to share your P2P WebRTC experiences.
