37 KiB
Native Messaging: Finally Bringing P2P to the Modern Browser
The Problem That Wouldn't Leave Me Alone
For years, I'd come back to the same frustrating question: why can't we just run peer-to-peer networking in a browser? Not WebRTC with its signaling server requirements, not WebSockets that are really just TCP wrapped in HTTP, but real honest-to-goodness P2P where two browsers connect directly to each other without anyone in the middle.
The browser sandbox exists for good reasons. If any website could open raw sockets, bind to ports, participate in UDP protocols, the security implications would be enormous. Malicious sites could run scanning tools, bypass firewalls, create botnets. The sandbox protects users from themselves and from attackers. This isn't a bug in browser design, it's a feature that's kept the web usable for decades.
But the restriction creates an enormous gap. Want to build a chat application where messages go directly between users? You can't. Want to create a collaborative editing tool without a central server? You can't. Want to make a file sharing app that doesn't require uploading to some cloud service first? You can't. Every real-time web application that's ever been built follows the same client-server pattern: all data flows through a server that you have to deploy, maintain, scale, and pay for.
I'd watched Hyperswarm emerge and mature. It was solving the hard P2P problems: distributed hash tables for discovery, UDP hole-punching for NAT traversal, the Noise protocol for encryption. People were building incredible decentralized applications with it. But they were all running in Node.js or Bun or some other server-side runtime. The browser remained locked out.
This bothered me more than it probably should have.
Finding the Way In
Chrome extensions have this feature called native messaging. It's been around since the early days, but the official documentation focuses on mundane use cases: integrating with password managers, connecting to desktop notification systems, that sort of thing. The technical capability underneath is much more powerful.
Native messaging lets an extension spawn a process outside the browser and communicate with it through standard input and standard output. The browser handles spawning the process, manages its lifecycle, and provides a clean message-passing interface. Nothing fancy, but effective.
The crucial realization hit me like a freight train: the native process can run whatever code it wants. JavaScript in the browser can't do P2P networking, but JavaScript running in a native process outside the browser can do absolutely anything. The extension becomes a bridge between the privileged world inside the browser and the powerful world outside.
flowchart TB
subgraph Browser["Desktop browser"]
Page["Web page<br/>window.BridgeSwarm"]
Ext["Extension MV3<br/>content.js → background.js"]
Page <--> Ext
end
Ext <-->|native messaging<br/>stdin/stdout · 4-byte LE + JSON| Host
subgraph Host["Native host Bare"]
HS["Hyperswarm<br/>P2P DHT"]
HB["Hyperbee<br/>key/value"]
HD["Hyperdrive<br/>file system"]
HC["Hypercore<br/>append-only log"]
AB["Autobase<br/>multi-writer"]
HDB["Hyperdb<br/>schema DB"]
CAP["Capabilities<br/>media · live ffmpeg encode"]
EX["Examples server<br/>127.0.0.1:4173"]
end
The native host runs Hyperswarm, which handles all the peer discovery through the distributed hash table. It manages the NAT traversal magic that lets connections work behind home routers and corporate firewalls. It performs the Noise protocol handshake to establish encrypted sessions. It runs all the data storage systems that Hyperswarm supports. Nothing P2P happens without the native host being involved.
The extension acts as the intermediary. The service worker maintains the persistent connection to the native host, handles routing messages between different tabs, tracks which swarm belongs to which tab, manages request-response pairs so asynchronous operations return to their correct callers, and deals with disconnections and reconnections. The content script that gets injected into web pages serves as the bridge between the page's JavaScript context and the extension's privileged context.
The injected API that developers use is the final piece. It creates the BridgeSwarm class, handles all the JavaScript-side event emission, and manages communication with the extension through postMessage. The API is intentionally clean and simple, something that feels familiar to anyone who's used a networking library before.
The Runtime: Why Bare Matters
The native host runs on Bare, a minimal JavaScript runtime that's dramatically smaller than Node.js. This was a deliberate choice for several reasons that affect both the security model and the practical deployment of BridgeSwarm.
Traditional Node.js clocked in at around sixty megabytes when you counted the runtime, its standard library, and all the dependencies needed to run even a simple application. For a tool that's meant to be installed on end-user machines, that's uncomfortably large. The installation process becomes complicated, updates are slow, and users reasonably question why they need to install an entire development environment just to run a browser extension.
Bare takes a fundamentally different approach. Instead of bundling everything you might possibly need, Bare provides only the essential primitives: process management, file system access, the network APIs, and a module loader. The entire runtime is under three megabytes. It starts instantly. It has almost no attack surface compared to the sprawling Node.js codebase. When you install BridgeSwarm, you're installing a tiny runtime that does exactly what it needs to do and nothing more.
The module loading system in Bare deserves special attention. It uses a hyperloader-based system that can load modules from various sources, including npm packages. This means we can use the same packages that work in Node.js, which gave us access to the entire Hyperswarm ecosystem without modification. The Hyperbee key-value store, Hyperdrive file system, Hypercore append-only log, Autobase multi-writer log, and Hyperdb database system all work identically in Bare as they do in Node.js. We get full compatibility with the established P2P stack without sacrificing the lightweight deployment that Bare enables.
Using Bare also simplifies the dependency story. The native host declares its dependencies in a package.json, Bare resolves those dependencies, and everything just works. There's no need to bundle, tree-shake, or compile anything. The installation script pulls down Bare if it's not present, resolves the package dependencies, and you're ready to go. This makes the installer dramatically simpler than it would be with a bundled Node.js solution.
The trade-off is that some Node.js APIs aren't available in Bare. If you need something from the extensive Node.js standard library, you might need to find an alternative package or implement it yourself. For BridgeSwarm's purposes, every module we need was either built for universal JavaScript or had a compatible alternative available. We never hit a situation where the Bare choice prevented us from doing something we needed to do.
One of the most compelling reasons to use Bare is that it runs consistently across platforms. The same JavaScript code that works on macOS works on Linux and Windows without modification. The native host doesn't care about your operating system, it just needs somewhere to run JavaScript. This makes the installation process universal rather than requiring different packages for different platforms.
The Modules That Make It Work
The native host leverages several interconnected modules from the Hyper ecosystem to provide complete P2P functionality.
Hyperswarm is the networking layer that handles peer discovery and connection establishment. It uses a distributed hash table where peers announce their interest in specific topics. When your application calls join on a topic, Hyperswarm announces to the DHT that you're interested in that topic. Other peers doing the same will be discovered, and Hyperswarm attempts to establish direct connections. This discovery mechanism is entirely decentralized with no central server required.
Corestore provides the storage foundation that the other data modules build upon. It's essentially a system for managing multiple Hypercore instances, each with their own cryptographic key. When you need to store data in Hyperbee or Hyperdrive, Corestore creates and manages the underlying Hypercore that those systems use. It handles the key management so you don't have to think about it.
Hypercore is an append-only log, similar to a blockchain but without the proof-of-work consensus. Data is added in sequence, cryptographically linked to previous entries, and can be verified by anyone with the core's public key. It's the fundamental data structure that Hyperbee and Autobase build upon. For P2P applications, Hypercore provides tamper-evident logging that can be replicated between peers.
Hyperbee builds on Hypercore to provide a B-tree key-value store. Think of it like Redis but distributed and peer-to-peer. You put key-value pairs in, you get them out, and the data replicates automatically between connected peers. The B-tree structure makes lookups efficient even with millions of keys.
Hyperdrive is a P2P file system built on Hyperbee. You can create files and directories, read and write content, and everything syncs automatically between peers who are interested in the same drive. It's like having a shared filesystem that requires no server, where anyone with the drive key can read and write.
Autobase is a multi-writer version of Hypercore. Regular Hypercore has a single writer, but Autobase allows multiple peers to append to the same log while maintaining a consistent ordering through a linearization mechanism. This is crucial for collaborative applications where multiple users might make changes simultaneously.
Hyperdb adds schema and query capabilities on top of these primitives. Rather than just storing raw key-value pairs, you define collections with specific fields. It handles the complexity of replication, conflict resolution, and querying so you can work with a familiar database-like interface while the P2P magic happens underneath.
How Messages Actually Flow Through the System
The communication between extension and host uses Chrome's native messaging protocol, which itself is beautifully simple. Each message gets serialized as JSON, prefixed with a four-byte little-endian integer indicating the message length, then written to standard output. The receiving side reads the first four bytes to figure out how many more bytes to read, parses the JSON, and processes it.
// What the wire format looks like:
// [4 bytes: length][N bytes: JSON]
// Sending a message from host to extension:
const message = JSON.stringify({ type: 'event', event: 'connection', payload: {...} });
const length = Buffer.alloc(4);
length.writeUInt32LE(message.length);
process.stdout.write(Buffer.concat([length, Buffer.from(message)]));
This is remarkably similar to how many other protocols work under the hood, but it gets the job done reliably and is easy to debug when things go wrong. Binary data gets base64-encoded in the JSON payload, which adds some overhead but keeps the implementation simple.
Here's what actually happens when your web page calls swarm.join("my-topic"):
sequenceDiagram
participant Page as Web Page (api.js)
participant Content as Content Script
participant Background as Service Worker
participant Host as Native Host
Page->>Content: postMessage({ type: 'bridge-swarm-bridge', payload })
Content->>Background: chrome.runtime.sendMessage()
Background->>Host: port.postMessage({ id: 'req_123', type: 'join', payload })
Note over Host: Hyperswarm.join(topic)<br/>Announces to DHT<br/>Begins peer discovery
Host->>Background: messenger.send({ id: 'req_123', type: 'response', payload: { ok: true } })
Background->>Content: sendResponse(payload)
Content->>Page: dispatchEvent('bridge-swarm-bridge-response')
Page->>Page: resolve Promise, user gets control back
The request ID tracking is crucial. When the web page makes a request, it doesn't know how long it will take. The background service worker assigns a unique ID, stores the resolve and reject functions in a Map, sends the message to the host, and then returns control to the page immediately. When the response comes back with that same ID, the background looks up the stored functions and resolves or rejects the promise. This makes the API feel synchronous to the developer even though there's a tremendous amount of asynchronous machinery happening underneath.
Now let's trace what happens when a peer connects:
sequenceDiagram
participant Host as Native Host
participant Background as Service Worker
participant Content as Content Script
participant Page as Web Page
Note over Host: Hyperswarm discovers peer<br/>Noise handshake<br/>Connection established
Host->>Background: messenger.send({ type: 'event', event: 'connection', payload: { connId, swarmId, peerInfo } })
Note over Background: Look up which tab owns swarmId<br/>Send only to that tab
Background->>Content: tabs.sendMessage(tabId, { type: 'bridge-swarm-event', payload })
Content->>Page: window.dispatchEvent('bridge-swarm-event')
Note over Page: api.js receives event<br/>Creates BridgeSwarmConnection<br/>Emits 'connection' event
Page->>Page: user callback fires with (conn, peerInfo)
The routing logic here is critical. Multiple tabs might each have their own BridgeSwarm instances with different swarm IDs. When the native host emits a connection event, the background needs to figure out which tab should receive it. Every time a BridgeSwarm instance initializes, it registers its swarm ID with the background. The background maintains a mapping of swarm IDs to tab IDs, so when events arrive, it can look up the correct destination.
The Code Behind It All
Let's look at what this actually looks like in practice. Here's how the native host handles incoming requests:
// native-host/host.js - simplified
const commands = {
init: async ({ swarmId, options }) => {
const swarm = new Hyperswarm(options);
swarm.listen(); // Accept incoming connections
swarms.set(swarmId, swarm);
// Track connections for this swarm
swarm.on('connection', (socket, peerInfo) => {
const connId = `conn_${Date.now()}_${Math.random().toString(36).slice(2,8)}`;
connections.set(connId, { socket, swarmId, peerInfo });
// Emit event back to extension
emit('connection', { connId, swarmId, peerInfo: serializePeerInfo(peerInfo) });
// Handle incoming data on this connection
socket.on('data', (data) => {
emit('data', { connId, swarmId, data: data.toString('base64') });
});
socket.on('end', () => {
connections.delete(connId);
emit('end', { connId, swarmId });
});
});
return { ok: true };
},
join: async ({ swarmId, topic }) => {
const swarm = swarms.get(swarmId);
const topicBuffer = Buffer.from(topic.padEnd(32, '\0')).slice(0, 32);
swarm.join(topicBuffer);
return { ok: true };
},
write: async ({ connId, data }) => {
const conn = connections.get(connId);
if (!conn) return { error: 'Connection not found' };
const buffer = Buffer.from(data, 'base64');
conn.socket.write(buffer);
return { ok: true };
},
destroy: async ({ swarmId }) => {
const swarm = swarms.get(swarmId);
if (swarm) {
swarm.destroy();
swarms.delete(swarmId);
}
return { ok: true };
}
};
The host maintains several key mappings. The swarms Map tracks all active Hyperswarm instances by their swarm ID. The connections Map tracks all active P2P connections, keyed by a unique connection ID. There's also a mapping from swarm IDs to connection IDs so the host knows which connections belong to which swarm.
Now here's what the web page API looks like from the developer's perspective:
// This is all you need to write as a developer
// Wait for the API to be ready
await BridgeSwarm.ready();
// Create a swarm instance
const swarm = new BridgeSwarm({ appName: 'my-chat-app' });
// Join a topic - this is how peers find each other
await swarm.join('some-topic-name');
// Handle incoming peer connections
swarm.on('connection', (conn, peerInfo) => {
console.log('New peer connected:', peerInfo.publicKey.slice(0, 8) + '...');
// Handle incoming data
conn.on('data', (data) => {
const message = new TextDecoder().decode(data);
console.log('Received:', message);
});
// Send data to this peer
conn.write(new TextEncoder().encode('Hello, peer!'));
});
// Broadcast to all connected peers
for (const conn of swarm.connections()) {
conn.write(new TextEncoder().encode('Hello, everyone!'));
}
// Clean up when done
await swarm.leave('some-topic-name');
swarm.destroy();
That ten lines of code does an enormous amount of work under the hood. It initializes a Hyperswarm instance, generates a cryptographic key pair, joins a distributed hash table topic, discovers other peers interested in the same topic, establishes encrypted connections to each peer, handles NAT traversal, manages connection lifecycle, and emits events when things happen.
The Data Storage Layer
Beyond just peer-to-peer networking, BridgeSwarm exposes the full Hyperswarm data stack to web pages. This means you can build applications that share not just messages but actual data structures.
// Hyperbee - key/value store like a P2P Redis
await BridgeSwarm.request('beePut', { key: 'username', value: 'alice' });
const result = await BridgeSwarm.request('beeGet', { key: 'username' });
console.log(result.value); // 'alice'
// Hyperdrive - P2P file system
const fileContent = 'Hello, world!';
const base64 = btoa(fileContent);
await BridgeSwarm.request('drivePut', { path: '/readme.txt', base64 });
const file = await BridgeSwarm.request('driveGet', { path: '/readme.txt' });
// Hyperdb - schema-based database
await BridgeSwarm.request('hyperdbInsert', {
collection: 'users',
doc: { id: 'user1', name: 'Alice', email: '[email protected]' }
});
const user = await BridgeSwarm.request('hyperdbGet', {
collection: 'users',
query: { id: 'user1' }
});
These data operations flow through the same message-passing infrastructure as everything else. The native host manages the storage, handles replication between peers if desired, and returns results to the web page.
The Troubles I Faced
Building this system taught me a lot about the gap between what should work in theory and what actually works in practice.
The event routing problem consumed weeks. I'd get connections appearing in the wrong tab, messages going to tabs that had already closed, duplicate events, missing events. The root cause was that I wasn't properly tracking which swarm IDs belonged to which tab IDs. The fix involved explicit registration: when a BridgeSwarm instance initializes, it sends a register message to the background that includes its swarm ID. The background maintains a Map where swarm IDs map to Sets of tab IDs. Events get filtered at both the background level (which tabs should receive this) and the api.js level (is this event for this specific swarm instance).
// The registration logic in background.js
const tabSwarms = new Map(); // tabId -> Set<swarmId>
const swarmRefCount = new Map(); // swarmId -> reference count
async function handleRegisterSwarm(message, tabId) {
const { swarmId } = message.payload;
if (!tabSwarms.has(tabId)) {
tabSwarms.set(tabId, new Set());
}
tabSwarms.get(tabId).add(swarmId);
// Track reference count for cleanup
const count = swarmRefCount.get(swarmId) || 0;
swarmRefCount.set(swarmId, count + 1);
}
File descriptor locking in Hyperdrive gave me endless headaches. Hyperdrive tries to acquire an exclusive lock on its storage file to prevent corruption from concurrent access. But when multiple rapid operations happen, sometimes the lock fails or conflicts with another operation. The error message "File descriptor could not be locked" became far too familiar. I ended up implementing fallback behavior where files get sent directly over the P2P connection rather than being stored in Hyperdrive first. The sending peer keeps the file in memory and transmits it when the receiving peer requests it. This actually turned out to be more reliable for the direct peer-to-peer use case anyway.
Syntax highlighting in the chat application was absurdly difficult to get right. The marked library for parsing markdown, highlight.js for syntax coloring, and the marked-highlight extension that should connect them all all load from external CDNs at different times. The initialization order was never consistent, and I'd frequently get errors about functions not existing yet. I eventually added polling logic that retries until the libraries are available, along with a fallback that applies highlighting after the HTML is already in the DOM. It's not elegant, but it works.
One subtle issue was how each tab got its identity. Hyperswarm generates a cryptographic key pair when you create the swarm, and you can't change it later. Initially, all tabs using the same application would end up with identical public keys, making it impossible to tell them apart. The solution was generating a unique swarm ID for each BridgeSwarm instance, which creates a separate Hyperswarm with its own independent key pair. Each tab now has its own distinct identity while still being able to discover and communicate with other tabs on the same topic.
Example Application: P2P Chat
Here's a complete working chat application that demonstrates the system in action. This is essentially what's in the chat-advanced example in the repository.
// Complete P2P chat in about 60 lines
const state = {
swarm: null,
connections: new Map(),
nickname: 'Anonymous',
publicKey: ''
};
// Initialize when user clicks Join
async function joinChat(topic, nickname) {
state.nickname = nickname;
// Create the swarm
state.swarm = new window.BridgeSwarm({ appName: 'chat-app' });
// Listen for connections
state.swarm.on('connection', handleConnection);
// Join the topic to discover peers
await state.swarm.join(topic);
// Get our public key
state.publicKey = await state.swarm.getPublicKey();
// Broadcast our presence
broadcastPresence();
}
function handleConnection(conn, peerInfo) {
const connId = Date.now() + '-' + Math.random().toString(36).slice(2, 8);
// Track this connection
state.connections.set(connId, { conn, peerInfo, nickname: 'Unknown' });
// Send handshake
conn.write(JSON.stringify({
type: 'handshake',
nickname: state.nickname
}));
// Handle incoming data
conn.on('data', (data) => {
const msg = JSON.parse(new TextDecoder().decode(data));
switch (msg.type) {
case 'handshake':
// Update peer's nickname
const connObj = state.connections.get(connId);
if (connObj) connObj.nickname = msg.nickname;
broadcastPresence();
break;
case 'chat':
displayMessage(msg.author, msg.content, msg.timestamp);
break;
}
});
// Handle disconnect
conn.on('end', () => {
state.connections.delete(connId);
broadcastPresence();
});
}
function sendMessage(text) {
const msg = {
type: 'chat',
author: state.nickname,
content: text,
timestamp: Date.now()
};
// Send to all connected peers
for (const [, conn] of state.connections) {
conn.conn.write(JSON.stringify(msg));
}
// Display our own message
displayMessage(state.nickname, text, msg.timestamp);
}
function broadcastPresence() {
const peers = Array.from(state.connections.values()).map(c => c.nickname);
updateUserList([state.nickname, ...peers]);
}
This example shows the full flow. Users join a topic, connections establish automatically, messages send directly between peers, and presence information syncs across the network. There's no server required.
Repository and Installation
The full implementation lives at https://git.ssh.surf/snxraven/BridgeSwarm. You can also find it on GitHub at https://github.com/anomalyco/BridgeSwarm.
For the quickest start, you can run the installer directly without cloning:
curl -fsSL https://install-bridgeswarm.honeypeer.com -o install.sh && bash install.sh
This downloads BridgeSwarm, builds the native host, packages the extension, and opens your browser to load it. The installer handles all the compilation and configuration automatically.
If you prefer manual installation, clone the repository and run the setup script:
git clone https://git.ssh.surf/snxraven/BridgeSwarm.git
cd BridgeSwarm
./scripts/install.sh
Then load the extension by opening chrome://extensions, enabling developer mode, clicking "Load unpacked," and selecting the extension/ folder.
What's Possible Now
With this system running in a browser, the types of applications you can build fundamentally change. A real-time chat application requires no server deployment, no database setup, no scaling configuration. Users connect directly, messages flow between them, and there's no infrastructure for you to maintain.
Collaborative document editing becomes achievable without a central coordination server. Multiple users can connect to the same topic, exchange CRDT-based updates through Hypercore replication, and see each other's changes in real-time. The data lives in the users' browsers, synchronized directly between them.
File sharing applications work without uploading files to some cloud service first. One user sends a file directly to another over the encrypted P2P connection. No intermediate server, no storage costs, no privacy concerns about uploading files to third-party services.
Gaming applications benefit from the low latency that direct connections provide. For games where latency matters less like turn-based strategy or card games the peer-to-peer model works excellently. Even real-time games can leverage client-side prediction to compensate for network delays.
The applications are bounded only by what you can imagine. Any software that traditionally requires a central server to coordinate users can potentially be rebuilt as a direct peer-to-peer application.
Any Page, Anywhere
One of the most powerful aspects of this system is that it works on absolutely any web page, whether that page is served over HTTPS from a production server or opened directly from your local filesystem. The extension's content script gets injected into every page the browser loads, which means any website can potentially become a P2P application without requiring the website operator to run any special infrastructure.
This opens up possibilities that simply weren't feasible before. Consider the humble comment section on a blog post. Traditionally, when someone leaves a comment, it gets stored in a database on some server, and anyone else viewing that page has to request those comments from that same server. The comment system is entirely dependent on the blog's server being up, being configured correctly, and not having been compromised. With BridgeSwarm, the comments could flow directly between readers of the page. No database required, no comment server to maintain, no single point of failure.
The implementation is remarkably simple from the website operator's perspective. You include the BridgeSwarm API script on your page, the same way you might include jQuery or any other library. When a reader opens the page, the extension injects the API automatically. If that reader leaves a comment, it gets broadcast to other readers currently viewing the same page. There's no server component to deploy, no database to manage, no moderation infrastructure to maintain. The comments live in the browsers of the people reading the page.
If you want persistence beyond the current session, you have options. The simplest approach keeps comments entirely in-memory across the currently connected peers. When everyone closes their browser, the comments disappear. This works perfectly for ephemeral discussions, live streams, temporary events, or any situation where you don't need the comments to survive after everyone's left.
But there's a more powerful option if you need persistence. Hyperdb is a schema-based P2P database that's available through the same API. You can store comments in Hyperdb, and they'll persist across browser sessions. The comments get replicated between peers who are online at the same time, so even if someone wasn't viewing the page when you posted, they'll sync up when they eventually visit. There's no central database server to run, but the data survives through the distributed replication. You define a schema with collections, insert documents, run queries, all through the BridgeSwarm API. The database lives in the native host's storage, replicated across any peers who choose to participate in that particular database.
The hybrid approach is particularly interesting. You could use pure P2P for real-time discussion where comments flow directly between current viewers, while simultaneously storing everything in Hyperdb for persistence. New visitors who weren't online when a comment was posted would still receive it through the database replication. The two systems complement each other rather than being mutually exclusive.
The same approach works for live collaboration features. Think about a documentation site where multiple people are reading the same page at the same time. With BridgeSwarm, you could add features where users can see who's currently viewing the page, leave inline annotations that sync in real-time, or even have a persistent collaborative session that continues as people come and go. All of this works without any server-side component beyond serving the initial HTML and JavaScript.
This applies to essentially any embeddable widget or interactive element. A stock ticker that updates peer-to-peer between viewers of the same page. A live sports scoreboard that syncs without a backend. A polling widget where votes broadcast directly between participants. A q&a section for a webinar that doesn't require the host to run any messaging infrastructure. If multiple people can view the same page, they can communicate directly through it.
The content script injection works through Chrome's standard content script mechanism. When you navigate to any URL, the extension's content script runs automatically unless you've disabled it in the settings. There's a configuration option to skip injection on file:// URLs if you're concerned about local testing, but by default the API is available everywhere. The first time a page uses the BridgeSwarm API, it initializes a swarm and connects to whatever topic the application specifies. From that point forward, any other user viewing the same page with the same topic will discover and connect to each other automatically.
The practical implications are significant for developers who want to add P2P features to existing websites. You don't need to convince anyone to host a WebSocket server or deploy a WebRTC signaling service. You don't need to sign up for a third-party real-time service or worry about their rate limits or pricing changes. You simply include a script tag and write your application logic. The P2P networking happens entirely between the browsers of the people using your site.
There are interesting implications for offline usage as well. If you're developing a web application that needs to work in areas with poor connectivity, the P2P model can actually improve resilience. When you have multiple users viewing the same page offline, they can share data between themselves without needing to reach a central server. The data doesn't flow through any infrastructure you control, which means you don't need to worry about server uptime, bandwidth costs, or traffic spikes.
The settings panel in the extension gives users fine-grained control over this behavior. You can disable the extension entirely when you don't want P2P functionality. You can choose to skip injection on file:// URLs if you're testing locally and don't want the behavior there. You can see the debug output if you're trying to understand why connections aren't forming. The extension respects user agency rather than silently doing things in the background.
This architecture also means that websites can be genuinely useful even with a very small audience. A P2P comment section with just two readers still works perfectly. A collaborative document edited by two people functions fine. You don't need thousands of concurrent users to justify the infrastructure cost because there is no infrastructure cost. The cost scales with zero.
Configuration and Defaults
The extension includes a settings panel accessible by right-clicking the extension icon. These settings control how BridgeSwarm behaves by default when a page requests a swarm, allowing site operators and users to customize the behavior without requiring code changes.
The default application name gets used when a page creates a swarm without explicitly specifying one. The maximum peers setting controls how many simultaneous connections a swarm will maintain, with zero meaning unlimited. Request timeout determines how long the API waits for a response from the native host before rejecting a promise. Ready timeout controls how long BridgeSwarm.ready() waits for the API to become available.
There's also a setting that defaults to enabled which skips injection on file:// URLs. This is useful for developers who want to test their pages locally without triggering the P2P behavior. When this setting is disabled, the API gets injected into local files just like any other page, allowing full testing of P2P functionality without deploying to a server.
The notify on disconnect option shows a browser notification when the native host disconnects unexpectedly, which is helpful for debugging but can be annoying in production. Debug mode enables verbose logging to the console, which makes it much easier to understand why connections aren't forming or why messages aren't being delivered. These settings persist across browser sessions, so once you configure them the way you like, they stay that way.
Honest Limitations
This isn't the solution for every situation. Users must have the extension installed, which introduces friction that not every project can absorb. You can't simply share a URL and expect it to work without the extension being present.
Some network configurations don't support UDP hole-punching. Symmetric NATs and certain restrictive firewalls can't be bypassed. In these cases, Hyperswarm falls back to relay servers, which reintroduces the server dependency we were trying to avoid and adds latency.
The peer-to-peer model doesn't scale efficiently to thousands of users. Broadcasting a message to ten thousand peers requires ten thousand individual connections, which is dramatically less efficient than one connection to a server that fans out efficiently. The sweet spot is groups of roughly ten to a few hundred simultaneous users.
The native host runs with the same permissions as the user who launches it. This is inherent to the native messaging architecture. Users need to trust that the extension and host they're installing are legitimate, which means the code must be open source and auditable.
The Path Forward
Several improvements could make this system more powerful. Mobile browser support would be valuable, but Chrome's mobile extension API is significantly more limited than the desktop version. Cross-browser compatibility with Firefox works through the same native messaging mechanism, so that's already achieved.
More sophisticated example applications would help developers get started faster. A shared whiteboard, a simple game, a collaborative code editor each would demonstrate different capabilities of the system and provide templates for developers to build from.
The data layer integration could be deeper. The Hyperdb wrapper is currently minimal, exposing only basic operations. A more complete wrapper library would make building database-backed applications much more approachable. Automatic replication could be added so that applications sync their data across connected peers without explicit configuration.
Connection establishment time could be optimized. There's a noticeable delay between joining a topic and actually being connected to discovered peers. Reducing this would make the system feel significantly more responsive.
The Core Idea
The browser was never designed for peer-to-peer networking. The sandbox, the security model, the entire architecture assumes a client-server world. But through native messaging, we've found a way to bring P2P capability to the browser without compromising the security model.
The architecture isn't elegant. There's a native process, an extension, multiple layers of message passing, routing logic, fallback behavior for edge cases. It's complex in ways that pure server-based solutions aren't. But it works, and what it enables makes the complexity worthwhile.
For years, building real-time web applications meant accepting the server as a necessary intermediary. Now there's another option. The code is there, the examples are there, the documentation is there. If you've ever wanted to build applications where users communicate directly without a server in the middle, the tools are finally available.