# Create a simple chat app
WARNING
# js-ipfs being discontinued
Development of the js-ipfs project (opens new window) is being discontinued to focus on Helia (opens new window), a leaner, more modular, modern implementation of IPFS in JavaScript scheduled for release in 2023. To learn more about Helia and the current state of IPFS in JS, see the blog post (opens new window).
Because of this, js-ipfs tutorials may be out of date, and will eventually be archived.
This how-to demonstrates a minimal chat app in js-ipfs entirely in the browser.
Here’s a quick demo of the app:
The heading shows which user is chatting and has a status indicator in the top left to let you know what kind of connection you have.
- Green means you're connected to the relay, even if it's via another peer.
- Yellow means you're only seeing direct peers (no other peer in the middle).
- Red means you have no peers (at least none using the chat application).
To see a live demo, start your IPFS daemon (open IPFS Desktop or enter ipfs daemon
in the CLI) and have a chat buddy do the same. Then you can both open the live demo (opens new window) and chat. Once our chat app gets some traction, you’ll be able to make new friends on the network.
# How it works
To get a minimal chat app going in your browser, you can download a copy of the chat app code, run the daemon, and open index.html
(instructions below).
To test and deploy your own version with your own nodes (recommended for deployments), follow the setup below for:
- Getting the code
- Discovery and connectivity set up to peers
- Advertising set up to let other peers know your users are there
- Publishing set up and staying connected to peers
Besides IPFS (with CIDv1) and JavaScript, our chat app uses these technologies:
- Libp2p (opens new window)’s WebRTC (opens new window)-Star and circuit relay (opens new window) for discovery and connecting (with two libraries: js-ipfs (opens new window), Bootstrap–with minified CSS), and Kubo (opens new window) for p2p circuit connecting with WebSockets.
- Some Python code that we supply for advertising.
- Libp2p’s experimental PubSub (opens new window) feature for publishing, with some tips for staying connected.
# Getting the code
There are two ways to get the code: You can download it or you can clone or fork TheDiscordian/browser-ipfs-chat (opens new window) from GitHub.
To download the code:
In the CLI, start your daemon with
ipfs daemon
.In another instance of the CLI, get the chat app code directory with:
ipfs get bafybeia5f2yk6td7ciroeped2uwfivo333b524t3zmoderfhl3xn7wi7aa
Change directories:
cd bafybeia5f2yk6td7ciroeped2uwfivo333b524t3zmoderfhl3xn7wi7aa
Use
open index.html
to see it in the browser.
A tab with the app opens and you’ll automatically begin connecting to nodes and looking for peers! Your home folder will contain the new directory with the HTML, Bootstrap, and JS files.
To test and deploy your own version, you’ll be editing index.html
.
Let's take a look at how this works.
# Peer discovery and connectivity
Browsers don’t automatically provide discovery and connectivity, so, without the code we provide, you can't listen for new peers and get access to the navigation system that the distributed hash table (DHT) (opens new window) provides. Our chat app provides discovery and connectivity in two ways:
- WebRTC-Star (opens new window) achieves direct browser-to-browser communication.
- Circuit relay (opens new window), maintains the connection with a relay in the middle.
This diagram demonstrates what a three-user network can look like.
The browser nodes can communicate with Kubo as well, so BrowserC doesn't have to be a browser at all, but instead could be a Kubo node!
# WebRTC-Star
We use WebRTC-Star (opens new window) nodes to help discover other peers to connect with directly, browser-to-browser. We use the js-ipfs (opens new window) and Bootstrap libraries. For Bootstrap, we’re only using minified CSS. If you want a newer version of js-ipfs (opens new window), you can use the latest version available from JSDelivr (opens new window).
If you're familiar with the concept of STUN (opens new window), it might be helpful to think of WebRTC-Star nodes as conceptually similar. Effectively, each connecting node is given a WebRTC-Star multiaddress (opens new window) that other nodes can use to discover and connect to your browser directly. This means that if you peer with someone using the star node, and the star node goes offline, you remain connected!
# Usage
Here’s how we connect to a star node, in the <script> of the html files:
ipfs = await Ipfs.create({
repo: 'ok' + Math.random(), // random so we get a new peerid every time, useful for testing
config: {
Addresses: {
Swarm: [
'/dns4/star.thedisco.zone/tcp/9090/wss/p2p-webrtc-star',
'/dns6/star.thedisco.zone/tcp/9090/wss/p2p-webrtc-star'
]
},
}
});
# Setup
Please note that this how-to uses example star nodes — however, those won't necessarily always be accessible. Currently, it's important to either find a reliable star node or host your own. You can host your own by following the instructions for a native setup (opens new window) or for a Docker container (opens new window) which includes Nginx (for SSL). If you opt for the native setup, we cover the Nginx reverse proxy process and SSL cert retrieval later in this post.
# p2p-circuit
WARNING
This section is currently only relevant for Kubo versions before v0.11.0, as it's about circuit relay v1. There is currently no solution available to directly replace it, though most of the work has been completed (opens new window).
WebRTC-Star is a very clean and effective method of P2P communications; however, sometimes NATs get in the way, so we use p2p-circuit (opens new window) to get around that.
Using p2p-circuit is helpful for peers behind tricky NATs (or a VPN, or anything really). If you're familiar with TURN (opens new window), it might be useful to think of the relaying of p2p-circuit as something similar.
# Usage
Once all the services for p2p-circuit are put together, connecting to the node can be achieved in a few different ways.
First, to connect on startup to only our node(s):
ipfs = await Ipfs.create({
config: {
Bootstrap: \[
'/dns6/ipfs.thedisco.zone/tcp/4430/wss/p2p/12D3KooWChhhfGdB9GJy1GbhghAAKCUR99oCymMEVS4eUcEy67nt',
'/dns4/ipfs.thedisco.zone/tcp/4430/wss/p2p/12D3KooWChhhfGdB9GJy1GbhghAAKCUR99oCymMEVS4eUcEy67nt'
\]
}});
Or you can add your own after, then manually initiate the connection:
await ipfs.bootstrap.add('/dns6/ipfs.thedisco.zone/tcp/4430/wss/p2p/12D3KooWChhhfGdB9GJy1GbhghAAKCUR99oCymMEVS4eUcEy67nt');
await ipfs.swarm.connect('/dns6/ipfs.thedisco.zone/tcp/4430/wss/p2p/12D3KooWChhhfGdB9GJy1GbhghAAKCUR99oCymMEVS4eUcEy67nt');
await ipfs.bootstrap.add('/dns4/ipfs.thedisco.zone/tcp/4430/wss/p2p/12D3KooWChhhfGdB9GJy1GbhghAAKCUR99oCymMEVS4eUcEy67nt');
await ipfs.swarm.connect('/dns4/ipfs.thedisco.zone/tcp/4430/wss/p2p/12D3KooWChhhfGdB9GJy1GbhghAAKCUR99oCymMEVS4eUcEy67nt');
If you're looking to do your own client without copying the example, ensure that you're also communicating with the announce channel, which is described under advertising (opens new window) below. This is the simplified version of the relevant code in the chat demo:
var ipfs; // store the IPFS node you're using in this variable
// processes a circuit-relay announce over pubsub
async function processAnnounce(addr) {
// get our peerid
me = await ipfs.id();
me = me.id;
// not really an announcement if it's from us
if (addr.from == me) {
return;
}
// if we got a keep-alive, nothing to do
if (addr == "keep-alive") {
console.log(addr);
return;
}
peer = addr.split("/")\[9\];
console.log("Peer: " + peer);
console.log("Me: " + me);
if (peer == me) { // return if the peer being announced is us
return;
}
// get a list of peers
peers = await ipfs.swarm.peers();
for (i in peers) {
// if we're already connected to the peer, don't bother doing a
// circuit connection
if (peers\[i\].peer == peer) {
return;
}
}
// log the address to console as we're about to attempt a connection
console.log(addr);
// connection almost always fails the first time, but almost always
// succeeds the second time, so we do this:
try {
await ipfs.swarm.connect(addr);
} catch(err) {
console.log(err);
await ipfs.swarm.connect(addr);
}
}
// process announcements over the relay network, and publish our own
// keep-alives to keep the channel alive
await ipfs.pubsub.subscribe("announce-circuit", processAnnounce);
setInterval(function(){ipfs.pubsub.publish("announce-circuit", "peer-alive");}, 15000);
# Setup
Like the star nodes, it will be important to host your own things, as the ones in this how-to could go offline at any moment.
For the purposes of this example, you'll need to do a few things on a server hosting your own Kubo (opens new window) node. You'll also need a working Nginx install setup, which will be used for SSL, which is a requirement for browsers.
First configure the Go node, enabling WebSocket (opens new window) support, and designate it as a relay so we can communicate with it from a browser by editing ~/.ipfs/config
to add the following settings:
{
"Addresses": {
"Swarm" : [
"/ip4/0.0.0.0/tcp/4011/ws",
"/ip6/::/tcp/4011/ws"
]
},
"Swarm": {
"EnableRelayHop": true
}
}
TIP
Restart your Kubo node however you normally would (such as by using systemctl --user restart ipfs
), and you're mostly set up! You've enabled regular WebSockets with relaying support; however, you still need secure WebSockets (outlined in the SSLsection below) — otherwise browsers won't be able to connect to us.
# Advertising
Using p2p-circuit can be a bit tricky. Once you connect to the relay from a browser, you're still not advertising that you're able to be reached through it! For this purpose, this how-to includes a Python script that runs alongside Kubo and advertises the browser js-ipfs peers it encounters over PubSub (opens new window) with a p2p-circuit multiaddress (opens new window).
Here is the Python script (opens new window). You can run it with python ipfs_peeradvertiser.py
. However, first ensure that you edit CIRCUITS with your own node's information, or you won't announce peers correctly, and they won't know how to use your relay to connect to other peers.
You can retrieve your own circuit info by running ipfs id
on your Kubo node to get your PeerID. Then form the circuit URL like so:
/dns6/YOURDOMAIN.COM/tcp/4430/p2p/YOUR\_PEERID/p2p-circuit/p2p/
You should see here where you fill out the domain name that you got the SSL cert for, as well as your node's PeerID. For the script, the leading and trailing slash are required.
WARNING
Ensure you specify DNS6 or DNS4, depending on if you're forming an IPv6 or IPv4 address. It's important to ensure that you use DNS, otherwise browser nodes likely won't be able to connect. Also note the port 4430; if you used a different one, you'll need to specify that.
# SSL and Nginx
So far you've setup WebRTC-Star and p2p-circuit without SSL (unless you used the WebRTC-Star Docker setup). If you want to use your nodes over the internet, with a browser, they need to support SSL. If you're using the defaults, currently WebRTC-Star should be running on port 9090 (no-SSL) and p2p-circuit will be on port 4011 (no-SSL). We're going to put those on port 9091 (SSL) and port 4430 (SSL), respectively.
First, ensure that Nginx is installed, then obtain and install Certbot (opens new window).
Create two files from the templates below. Ensure that you're editing entries like YOURDOMAIN.COM with the full domain (including subdomain) that you plan to use for your services.
/etc/nginx/sites-available/ipfs (p2p-circuit, 4430(SSL) ➡ 4011)
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream ipfs {
server 127.0.0.1:4011;
}
server {
server_name YOURDOMAIN.COM;
listen 4430 ssl;
ssl_certificate /etc/letsencrypt/live/YOURDOMAIN.COM/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/YOURDOMAIN.COM/privkey.pem;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://ipfs;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
}
-----
/etc/nginx/sites-available/star (WebRTC-Star, 9091(SSL) ➡ 9090)
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream star {
server 127.0.0.1:9090;
}
server {
server_name YOURDOMAIN.COM;
listen 9091 ssl;
ssl_certificate /etc/letsencrypt/live/YOURDOMAIN.COM/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/YOURDOMAIN.COM/privkey.pem;
location {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://star;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
}
}
In this example, we're accepting SSL on port 4430 — this is our "WSS port" (WebSocket Secure) — and then passing it to the unsecured port locally on 4011 — this is our "WS port". If you want to connect to this node from a browser, you'd use port 4430.
Next, run the following:
sudo systemctl stop nginx
sudo certbot -d YOURDOMAIN.COM --standalone
Edit YOURDOMAIN.COM to the domain you want a cert for, if you need multiple, fill in multiple or run the command multiple times
Next, run:
sudo ln -s /etc/nginx/sites-available/ipfs /etc/nginx/sites-enabled/ipfs
sudo ln -s /etc/nginx/sites-available/star /etc/nginx/sites-enabled/star
sudo systemctl start nginx
Nginx is now operating as a reverse proxy, giving you secured WebSockets!
# Communication
Whew! Since you made it this far, you might be wondering "what is communication like?" We're rolling out PubSub (opens new window) for communication. It still has a couple issues to handle manually, so we’ll walk you through those.
# PubSub
With PubSub, you can subscribe to topics and retrieve any messages posted to those topics. In js-ipfs, you can set a callback function, which gets called whenever a message is received:
function echo(msg) {
msg = new TextDecoder().decode(msg.data);
console.log(msg);
}
await ipfs.pubsub.subscribe("example\_topic", echo);
To publish use:
await ipfs.pubsub.publish("example\_topic", "Hello world\!");
This is effectively what the chat demo is doing: It's subscribing to a global topic (named discochat-global
), and relaying the messages that people type over PubSub.
# Staying connected
So let's say you've done everything correctly. You're able to find peers using WebRTC-Star and p2p-circuit — awesome! However, you might find your connections expire, and you're unable to restore them, so here's how to mitigate these issues:
# Staying connected to peers
We stay connected to peers in a couple of ways. The first way is more direct: by subscribing to and sending a keepalive
announcement over discochat-keepalive
every 4 seconds:
setInterval(function(){sendmsg("1", prefix+"keepalive");}, 4000);
setInterval(checkalive, 1000);
This should help ensure that you give peers looking to chat a high priority. Additionally, we report over announce-circuit every 15 seconds to make sure we keep a connection to the circuit relay so we can connect to peers stuck behind a NAT. That's accomplished like so:
// process announcements over the relay network, and publish our own keep-alives to keep the channel alive
await ipfs.pubsub.subscribe("announce-circuit", processAnnounce);
setInterval(function(){ipfs.pubsub.publish("announce-circuit", "peer-alive");}, 15000);
TIP
A simplified version of processAnnounce
is found under p2p-circuit above.
The Python script on the circuit relay will report a keepalive
every 4 seconds. You may have noticed we're reporting "peer-alive" instead of "keep-alive"; this is to separate peer requests from relay requests, making it easier to tell when we no longer see a relay.
# Staying connected to the circuit relay
Outside of the simplified version of processAnnounce
, in the real version there are a few variables used for tracking "keep-alive" and "peer-alive". These are lastAlive
and lastPeer
, respectively. We even track the last time you bootstrapped with lastBootstrap
. Using all this, we display the yellow status when you're only connected to peers (tracked via lastPeer), and if we don't see a keep-alive for 35 seconds (and you haven't attempted a bootstrap in 60 seconds), we attempt to re-connect to the bootstrap relay (and display a red status). We accomplish this like so:
const bootstraps = \[
'/dns6/ipfs.thedisco.zone/tcp/4430/wss/p2p/12D3KooWChhhfGdB9GJy1GbhghAAKCUR99oCymMEVS4eUcEy67nt',
'/dns4/ipfs.thedisco.zone/tcp/4430/wss/p2p/12D3KooWChhhfGdB9GJy1GbhghAAKCUR99oCymMEVS4eUcEy67nt'
\];
var lastAlive = 0; // last keep-alive we saw from a relay
var lastPeer = 0; // last keep-alive we saw from another peer
var lastBootstrap = 0; // used for tracking when we last attempted to bootstrap (likely to reconnect to a relay)
// if reconnect is true, it'll first attempt to disconnect from the bootstrap nodes
async function dobootstrap(reconnect) {
now = new Date().getTime();
if (now-lastBootstrap \< 60000) { // don't try to bootstrap again if we just tried within the last 60 seconds
return;
}
lastBootstrap = now;
for (i in bootstraps) {
if (reconnect) {
try {
await ipfs.swarm.disconnect(bootstraps\[i\]);
} catch (e) {
console.log(e);
}
} else {
await ipfs.bootstrap.add(bootstraps\[i\]);
}
await ipfs.swarm.connect(bootstraps\[i\]);
}
}
// check if we're still connected to the circuit relay
function checkalive() {
now = new Date().getTime();
if (now-lastAlive \>= 35000) {
if (now-lastPeer \>= 35000) {
document.getElementById("status-ball").style.color = "red";
} else {
document.getElementById("status-ball").style.color = "yellow";
}
dobootstrap(true); // let's try to reconnect
} else {
document.getElementById("status-ball").style.color = "lime";
}
}
setInterval(checkalive, 1000);
WARNING
The above code should be used with the full version of processAnnounce
, as it relies on lastAlive
and lastPeer
, which aren't updated in the simplified version.
# More resources
If you were successful in following this entire guide, you now have the ability to deploy powerful IPFS apps that run entirely in the browser and leverage decentralized p2p whenever you can! To learn more, check the resources below:
- js-ipfs/docs/BROWSERS.md (opens new window)
- js-ipfs/docs/CONFIG.md (opens new window)
- js-ipfs/docs/core-api (opens new window)
- js-ipfs/examples/circuit-relaying (opens new window)
- js-libp2p-webrtc-star (opens new window)
Thanks to TheDiscordian (opens new window) for originally creating this material as a post in IPFS Blog & News (opens new window).