Decentralized... Backend? Building an Angular dApp With OrbitDB and IPFS
I built a decentralized, p2p message client with no backend server that runs entirely in the browser.
So I'm a huge proponent of the decentralization of the web, and not just in the realm of cryptocurrency and NFT's. I think the future of the web is true decentralization, and while Web3 seems to have already developed a pretty bad association with gorilla profile pictures, there's more to it than just making money. Not too long ago I wrote a post about IPFS, a p2p file sharing protocol similar to torrenting. But I recently discovered a really impressive library built on top of it that I think has a lot of potential.
OrbitDB
OrbitDB is a pre-alpha library that harnesses the power of IPFS's p2p filesharing and applies it to databases.
But what does that mean exactly?
Let's say you want to create a chat application. Normally, you'd have to spin up some type of database that runs on a server, and then write an API at the client level to interact with it. But with OrbitDB, this database is serverless (No not AWS Lambda serverless. I mean real, actual, 100% browser-based, serverless).
When you send a message, it sends it to a database, and that database exists on your machine. And if other people wanted to join your "chat room," they would then download the database via IPFS, and together, everyone in the chatroom would be able to update and sync it between eachother.
It's really quite clever, and to be honest I'm surprised it hasn't got more attention.
Orbit Chat
So I decided I wanted to get my hands dirty with this, and had a few ambitious project ideas pop in my head rather quickly, but I found there were limited sample projects or working apps I could reference. So I decided to try and build something to test the waters: a decentralized message client!
Setup
First we need to install ipfs-core and orbit-db to our Angular project.
npm install ipfs-core orbit-db --save
You'll want to add node_modules/orbit-db/dist/orbitdb.js to your scripts defined in angular.json as well.
Then may notice that you get compilation errors saying that global was not found or console messages saying that process is not defined. We can fix these by adding a custom webpack configuration, and specifying it in angular.json (see the repository for full code).
npm install @angular-builders/custom-webpack node-polyfill-webpack-plugin --save-dev
extra-webpack.config.ts
import * as NodePolyfillPlugin from 'node-polyfill-webpack-plugin';
module.exports = {
node: { global: true },
plugins: [new NodePolyfillPlugin()]
};
You also may need to set skipLibCheck: true
in your tsconfig.json
.
IPFS Service
import { Injectable } from '@angular/core';
import { IPFS, create } from 'ipfs-core';
import { BehaviorSubject, } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class IpfsService {
private _ipfsSource = new BehaviorSubject(null);
private _createIPFSNodePromise: Promise;
private get LocalIPFS() {
const getter = async () => {
let node = this._ipfsSource.getValue();
if (node == null) {
console.log("Waiting for node creation...")
node = await this._createIPFSNodePromise as IPFS
this._ipfsSource.next(node);
}
return node;
}
return getter();
}
constructor() {
const ipfsOptions = {
repo: './ipfs',
start: true,
preload: {
enabled: false
},
EXPERIMENTAL: {
ipnsPubsub: true,
},
config: {
Addresses: {
Swarm: [
'/dns4/wrtc-star1.par.dwebops.pub/tcp/443/wss/p2p-webrtc-star/',
]
},
}
};
this._createIPFSNodePromise = create(ipfsOptions);
}
async getLocalNode() {
return await this.LocalIPFS;
}
}
So the important part here is the ipfsOptions
. Notice that we set ipnsPubsub
, this is an experimental feature within ipfs.js, but it has worked without any issues for me. It is used for getting realtime updates for changes in IPFS data.
Also notice that we define a Swarm, and have a funny looking address listed there. That is the signal server that we're using. It's bit long and tricky, but essentially connecting directly to other people within a browser environment is difficult due to port accessibility, so we need a centralized signal server to tell users how to find eachother. This is a public test one, so uptime isn't guaranteed, but it's been pretty functional from my tests so far.
I know, I know. This isn't very "decentralized". But this is really only a limitation for browsers, and you're still trasmitting information directly between eachother, with no middleman. The signal server just helps you find peers. If you properly install IPFS within a desktop environment, this isn't a requirement.
OrbitDB Service
import { Injectable } from '@angular/core';
declare var OrbitDB: any;
@Injectable({
providedIn: 'root'
})
export class OrbitdbService {
messages = new Array();
private _localInstance: any;
private _database: any;
async loadLocalInstance(IPFS: any) {
this._localInstance = await OrbitDB.createInstance(IPFS);
}
async createDatabase() {
let peerId = await this._localInstance.identity.id;
this._database = await this._localInstance.feed("orbitchat.messages." + peerId, {
accessController: {
write: ["*"]
},
localOnly: false,
overwrite: true,
replicate: true
});
this._database.events.on('replicated', () => {
this.updateMessages();
});
this._database.events.on('ready', () => {
this.updateMessages();
});
this._database.events.on('write', (address: any, entry: any, heads: any) => {
this.updateMessages();
});
await this._database.load();
const address = await this._database.id;
return address;
}
async connectToDatabase(address: string) {
await this._database.close();
this._database = await this._localInstance.open(address);
this._database.events.on('replicated', () => {
this.updateMessages();
});
this._database.events.on('ready', () => {
this.updateMessages();
});
this._database.events.on('write', (address: any, entry: any, heads: any) => {
this.updateMessages();
});
await this._database.load();
}
updateMessages() {
const items = this._database.iterator({ limit: -1 }).collect().map((e: any) => e.payload.value);
let i = 0;
items.forEach((e: any) => {
if (i < this.messages.length) {
this.messages[i] = e;
}
else {
this.messages.push(e);
}
i++;
});
}
async sendMessage(message: string) {
if (this._database) {
await this._database.add(message);
}
}
}
Here we can initialize an OrbitDB instance by giving it the IPFS object we created from our previous service. Then all we have to do is call the feed function to initialize an OrbitDB database. There a few different types of databases you can use, but feed works in our case.
We can then instantiate listeners that will update our messages. ready is thrown when the database has loaded all local data, write is thrown when we, ourselves, add to the database, and replicated is thrown after we finish replicating the database from a peer. Here is the full list of events that can be thrown.
App Component
import { Component } from '@angular/core';
import { IpfsService } from './services/ipfs/ipfs.service';
import { OrbitdbService } from './services/orbit-db/orbitdb.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'orbit-chat';
uri = "";
status = "Initializing IPFS...";
constructor(private ipfsService: IpfsService, public orbitDbService: OrbitdbService) { }
async ngOnInit() {
let IPFS = await this.ipfsService.getLocalNode();
this.status = "Initializing OrbitDB...";
await this.orbitDbService.loadLocalInstance(IPFS);
this.status = "Creating OrbitDB database...";
this.uri = await this.orbitDbService.createDatabase();
this.status = "Connected!"
}
async connectButtonClick() {
this.status = "Connecting to OrbitDB database...";
await this.orbitDbService.connectToDatabase(this.uri);
this.status = "Connected!";
}
messageInputEnterKeydown(event: any) {
const inputValue = event.target.value;
event.target.value = '';
this.orbitDbService.sendMessage(inputValue);
}
}
In the app component we tie it all together. And voila! We have a decentralized, serverless, p2p message client.