Decentralized, offline-first, cmd-line chat app in < 100 lines of code

developers Jun 04, 2019

Update: some of the methods below are out of date. For a good overview of the latest and greatest methods, be sure to read the Tour of Textile.

Here’s the quickest way to build an offline-first chat app for Nodejs

Light Work
Photo by Émile Perron / Unsplash

To quote, we live in a disconnected & battery powered world, but our technology and best practices are a leftover from the always connected & steadily powered past. At Textile, we think a new approach to app development that embraces modern, decentralized web technologies while at the same time promoting user-centered data control is the way forward. But decentralized app-development is still in its infancy, and has been notoriously difficult to get started with. That’s where Textile’s growing suite of developer tools comes into play — making it easier for developers to build decentralized, offline-first, and privacy-preserving apps using the same types of tools they’ve come to expect from more traditional centralized frameworks.

To show you just how easy it is to build useful apps with Textile, today’s post is going to cover building a minimalist, cmd-line chat app, that is fully decentralized, supports offline messaging, peer discovery, and more. And we’re going to do it in less than 100 lines of ‘client-side’ code. As far as ‘hello world’ example apps go, this one’s actually pretty useful. Sound too good to be true? Let’s find out?!

For the impatient, here’s the code in full to play with.

Getting Started

So first off, we’re going to be building our app on top of the Textile ecosystem of decentralized developer tools. In particular, we’re going to take advantage of the Textile desktop tray app to run our Textile/IPFS node, the js-http-client library to interface with said node, and a few other Nodejs libraries to help flush out the terminal-based user interface. So let’s start by downloading and installing the latest desktop app on your machine. For the purposes of this demo, you might as well create a new Textile account while you’re at it. The on-boarding screens in the app should help get you started. But before you dig in too much, I highly recommend you check out the Tour of Textile. We’ll be referencing some of the sections in that document throughout this blog post, plus it explains what even is a Textile wallet account? Why you might want to create one? And how to interact with it…


Now that you are an expert in Textile technologies, and have downloaded and installed the tray app, let’s start building. First things first, we’ll create a minimal Nodejs project and install some initial dependencies:

mkdir txtl
cd txtl
yarn init

Follow the prompts to produce your package.json file, mine looks like this:

  "name": "txtl",
  "version": "1.0.0",
  "description": "A simple cli chat app using Textile",
  "main": "index.js",
  "author": "Textile",
  "license": "MIT"

If you want to version control this project, you can also git init and curl > .gitignore to initialize a repo with a useful .gitignore file.


Next, let’s add some dependencies. We mentioned js-http-client earlier (this is how we’ll access the Textile tray app’s API), so let’s grab that one along with three additional libraries that simplify making cmd-line apps in Nodejs (cac — for building our cmd-line interface, chalk —for terminal string styling and colors, and node-emoji — because we need emojis!):

yarn add @textile/js-http-client cac chalk node-emoji

Ok, with that stuff in place, let’s think about what we are going to build. We’re basically going to emulate the Start a chat section of the Tour of Textile, except in our case, one (or both) of our peers will be using our custom Nodejs chat interface. So if you take a look at that part of the tour, you’ll see that we have a very simple cmd-line chat app that you start with textile chat and that receives input on stdin, and writes outputs to stdout, with some nice colors and things to make it a bit more fun. Pretty straightforward right? So let’s get started.

Setup the Interface

First, we’ll create the cmd-line interface, then we’ll add the actual Textile-based code, and then we’ll add some colors and tweaks. We’ll add the required imports along the way, so you can see where each change is coming from. So start by creating an index.js file in your main project folder (I use VSCode, but you can stick with whatever editor you like):

Nothing too fancy here, we’re pretty much just copy-pasting from the cac docs! We’re mimicking the interface from the go-textile cmd-line client, so we’ve added the --thread option here as well. Ok, next we’re going to add some interactivity to our cmd-line tool using the readline module. Here are the updates we need to make:

We’re simply specifying our readline interface, and then echoing our input to the terminal each time we enter a new line. Not very useful yet… so let’s add some Textile functionality to the mix to make it useful! We’re going to assume you already have a go-textile or Desktop tray app peer running, so we’ll just dive right in to interacting with the peer.

Interacting with Textile

Ok, so this part here isn’t totally complete, but here’s what’s going on:

  • On line 9 we’ve imported the default textile instance from the Textile library. This instance assumes you are running the Textile REST API on (which it is by default). If you are running on some other port, you’ll have to import Textile (the class), and create a new instance with something like const textile = new Textile({ url: …, port: …, version: 0 }).
  • On line 27 we’ve specified that on each new input from the terminal, we’ll send the message to our specified thread, and then print out a nice prompt on the next line.
  • Lines 31–42 are simply fetching our local profile, and using our display name (if it exists, otherwise the first part of our address) as our input prompt. Then, we’re subscribing to all TEXT events on the specified thread, and… well doing nothing so far.

Subscribe to Updates

So now that we have that mostly setup, let’s actually do something with that subscription API, so that we can be notified when new chat messages are added to the thread.

This is by far the most involved step, and note that we haven’t done any error checking or anything like that to keep things simple. We’ll add that in a bit… For now, note that all js-http-client streaming endpoints return a ReadableStream (in Nodejs and the browser thanks to some isomorphic magic!). This stream can be piped to a transform stream or any writable stream, and you can grab a reader to read updates from the stream, which is what we do here. In the above example (which is very similar to the MDN web docs example), we’re simply recursively calling read until the stream is done (which should stay open as long as your peer is running). Any time we do have a new update, we grab it, figure out the user who added it, and if it wasn’t us, we clear the current line, add the new message, and log a new prompt. Simple!

Make it Purdy…

The above example is ready to go, but if we really want to emulate (and build upon) the go-textile version, we need a few final touches:

There, now we have emoji powers and some nice colors to improve our chat experience! You should be able to run the example now. So head back over to the Start a chat example in the Tour of Textile, and in the step when you fire up the default chat session (textile chat --thread=”12D3K…), use your new custom chat app instead: ./index.js --thread="12D3K… (after running chmod +x .index.js).

Wanna add even more power to your new cmd-line app? Add the following to your package.json file to specify the binary cmd-line tool:

  "bin": {
    "txtl": "index.js"

Awesome, now when you run something like yarn add -g txtl, you’ll have a handy dandy cmd-line chat tool at your disposal! But wait, there’s more! Wanna do this whole thing in Typescript instead? Easy.

Typescript Superpowers!

Start by adding adding some devDependencies:

yarn add --dev typescript @types/node @types/node-emoji

And then, once those are done installing go ahead and initialize the package with a tsconfig.json file:

yarn tsc --init

You can probably leave that with its defaults, but I’d recommend at least enabling declaration and sourceMap, and setting outDir to "./dist".

Now write some Typescript. There are hardly any changes to make, but here’s a gist to a fully-working typescript version with a few extra checks in there for good measure.

Now, simply run yarn tsc, and you’ll have your freshly generated commonjs-style Javascript code, along with (stub) declaration files. If you really did want to publish this project, you’d probably want to update your bin entry, and add some sort of prepare script to your package.json file, but I’ll leave that as an exercise for the reader.

What about offline first?

We haven’t really mentioned the offline first bit yet, have we? First, why offline first? There are lots of great reasons, particularly most of the world’s daily use of the web continues to be driven by mobile, bandwidth-strapped, and battery powered interactions. But as a first-hand example… I’m writing this post while remote-working in a part of the world with extremely patchy Internet. The Medium web-ui is actually not usable, so I’m writing this in VSCode, and will eventually upload the file to Medium for posting. Not ideal. Chat apps, and other collaborative tools suffer from this issue even more! What if the peer you are chatting with isn’t online? Or your Internet cuts out mid-conversation? What if you’re both connected to the same local mesh network, but without external Internet access? Shouldn’t you still be able to chat with someone?

That’s where offline-first comes into play, and my local Textile peer (thanks to IPFS) handles this for me like a champ. If I’m writing a message while offline, my local peer will add the message to my local IPFS instance, and the next time I’m online, it will announce it to my peers (as far as UX is concerned, it feels like the message was ‘sent’). Even better still, if my peers have registered with a cafe, it’ll send my message to my peers’ inboxes, so they can fetch it the next time they’re online. At the same time, any messages waiting for me will be fetched automatically, ready to be read and interacted with. For things like chat, photos, and files, this can mean staying in touch with friends and family, even when your Internet connection is spotty at best.

The best part? The above scenario happens in an entirely decentralized way. There are not centralized servers required (even cafes are just additional peers connected to the decentralized IPFS network). This would even work in areas without access to the wider Internet. By using Textile as your chat ‘back-end’, you get all of these offline-first features ‘for free’. It just works. Check out the Textile docs to learn more about Textile’s approach to offline first, and how it can work for your next application.

Next steps

If you liked that, don’t forget to check out some of our other libraries that are built around Textile’s tray app and our js-http-client, like photos-desktop and desktop. We also have a great community of builders on our community slack channel, so jump in if you have any questions about using IPFS in your desktop apps.

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.