Skip to main content

Build a decentralized chat

In this tutorial, we are going to build a small chat app using Actyx. You might be tempted to skip it because you are not building chats in real-life — give it a chance. The techniques that you will learn in this tutorial are fundamental to building anything on the platform, and mastering them will give you a good understanding of its capabilities.

Before we start#

The tutorial is divided into three sections:

You don’t have to complete all of the sections at once to get value out of this tutorial. Try to get as far as you can — even if it’s just one or two sections.

Prerequisites#

To get the most out of this tutorial, it is helpful if you have basic knowledge of HTML, CSS, and TypeScript. But since this is a fairly simple example you should be able to follow along even if you are coming from a different programming language.

Typescript in 5 minutes

If you haven't worked in TypeScript before, we can recommend this guide to learn the basics.

Set up for the tutorial#

Now, let's get right to it. In this section, we will install Actyx and set up a basic project for our chat.

Install Actyx#

  • Download Actyx for Windows
  • Double click and run through the installer
  • Start Actyx via the Start Menu; a tray icon will appear
Problems installing?

If you have any problems check out the Actyx installation guide.


Set up the project#

In order to run, test and build the chat app you are going to need Node.js and npm, which you can install from here.

We are now going to set up a simple web app project using Parcel. Somewhere on your computer create a directory called chat.

In that directory create a file called package.json and add the following content:

{
"name": "decentralized-chat",
"version": "1.0.0",
"description": "A decentralized chat",
"scripts": {
"build": "tsc && parcel build index.html --public-url .",
"start": "tsc && parcel index.html",
"test": "server-test start 1234 'cypress run'"
},
"dependencies": {
"@actyx/pond": "^2.0.1"
},
"devDependencies": {
"@types/node": "^14.0.27",
"cypress": "^6.8.0",
"parcel-bundler": "^1.12.4",
"start-server-and-test": "^1.12.1",
"typescript": "^3.9.7"
}
}

Create another file called tsconfig.json with the following content:

{
"compilerOptions": {
"esModuleInterop": true,
"sourceMap": true,
},
}

Now create a file called index.html and add the following:

<html>
<head>
</head>
<body>
<p>A chat is coming soon!</p>
</body>
<script src="./index.js"></script>
</html>

Finally, create a file named index.ts with the following content:

console.log('Hello, world!')

To test that everything works, open a terminal, navigate to the chat directory and run npm install and then npm run start. This is what you should see in your terminal.

npm run start

If you now navigate to http://localhost:1234 in your browser and open the Developer Tools you should see this:

Chat in browser

Help, I’m stuck!#

If you get stuck, ping us on our Discord Chat or e-mail us at [email protected].

What is Actyx?#

Actyx is a platform for building highly resilient software systems distributed across a swarm of networked devices. Specifically you can:

  1. Implement business logic as low-code twins
  2. Include your twins in apps running on devices
  3. Forget about the network and data persistence

Actyx enables a completely decentralized architecture that allows you to build apps that always runs. Your apps always run because they run locally on the device and only interact with the locally running Actyx software.

Twins and Apps

Enough theory! Let's jump right in.

Building the chat#

Let's now have a look at how Actyx makes it super simple to build a decentralized chat.

To build and run our chat we need to do three things:

  1. Install Actyx on each device Already done!
  2. Implement our chat logic as a twin
  3. Integrate the twin into an app

Chat logic#

Our chat logic is simple. Anyone can send messages and receives messages sent by all others. When someone joins the chat, they receive all past messages that were sent before they joined.

The way to implement this using Actyx is to write a so-called twin. A twin is a state-machine. It has a state which it updates when it receives information from elsewhere.

Let's start by defining types for the chat twin's state and the events it can receive. In this case, we have one type of event which is simply a message (string). The state of the twin is an array of strings. In the index.ts file, add the following two lines of code:

type ChatEvent = string
type ChatState = ChatEvent[]

When a twin first starts up, it won't have any messages in memory. So let's give it an empty array as the initial state:

const INITIAL_STATE: ChatState = []

Now to the chat twin's logic, i.e. how to calculate the chat state, which we will show to the user, from the events we have received. We implement it as a so-called onEvent function. This function simply adds any message it received as a ChatEvent to our ChatState:

function onEvent(state: ChatState, event: ChatEvent) {
state.push(event)
return state
}

This is the complete chat logic. Let's now turn this into a fish.

The chat fish#

Twin == Fish

The Actyx library that allows you to write twins, actually calls them Fishes. You will see below, but Twin == Fish!

A fish, aka twin, is defined as an object with a couple of properties. You must provide the fish with an ID, an initial state, the onEvent function and information about where to get the chat messages from, a so-called event stream tags.

First, add the following imports to the top of the index.ts file:

import { FishId, Pond, Fish, Tag } from '@actyx/pond'

Now that we have done that, we create the tag for our chat messages and then define the fish itself:

const chatTag = Tag<ChatEvent>('ChatMessage')
const ChatFish: Fish<ChatState, ChatEvent> = {
fishId: FishId.of('ax.example.chat', 'MyChatFish', 0),
initialState: INITIAL_STATE,
onEvent: onEvent,
where: chatTag,
}

That's it. You can now import this fish into any application and it will automatically be synchronized across your network.

The user interface#

Lastly, we need to build a user interface and hook up our fish. Let's implement a very simple user interface showing the chat messages, an input field to type a message and a button to send the message.

Open up the index.html file and adjust the contents of the head and body sections as follows:

<html>
<head>
<title>Chat App</title>
<style>
body {
padding: 20px;
}
pre {
height: 300px;
padding: 10px;
background-color: #d9d9d9;
overflow-y: auto;
}
button {
margin-top: 10px;
}
button,
input {
width: 100%;
height: 30px;
}
</style>
</head>
<body>
<pre id="messages"></pre>
<input id="message" type="text" />
<button id="send">send</button>
</body>
<script src="./index.js" type="text/javascript"></script>
</html>

The last thing we have to do is to hook up the user interface to the fish. We want to

  1. Show all chat messages, i.e. fish's state in the pre element
  2. Send out a chat message event when the user clicks the Send button

In the index.ts file, add the following code:

Pond.default()
.then((pond) => {
// Select UI elements in the DOM
const messagesTextArea = document.getElementById('messages')
const messageInput = <HTMLInputElement>document.getElementById('message')
const sendButton = document.getElementById('send')
function clearInputAndSendToStream() {
// When click on send button get the text written in the input field
const message = messageInput.value
messageInput.value = ''
// Send the message to a stream tagged with our chat tag
pond.emit(chatTag, message)
}
sendButton.addEventListener('click', clearInputAndSendToStream)
// Observe our chat fish. This means that our callback function will
// be called anytime the state of the fish changes
pond.observe(ChatFish, (state) => {
// Get the `pre` element and add all chat messages to that element
messagesTextArea.innerHTML = state.join('\n')
// Scroll the element to the bottom when it is updated
messagesTextArea.scrollTop = messagesTextArea.scrollHeight
})

To test that everything works navigate to the chat directory, run npm run start and open http://localhost:1234. You should now see the chat app.

At this point you are chatting by yourself. What's the point?

The power of local-first#

What Actyx allows you to do now is run this chat app on as many computers as you want in your network. Actyx will automatically synchronize the chat twin across all those devices.

The power of this is that you do not have to worry about programming the network, nor about setting up some central database for storage. You only think: local-first.

And, your user can use the app whenever his device is on. Even when he is in airplane mode. Actyx will sync the twin when he gets back into the network. This is local-first.

And just like that you have built a decentralized app that would traditionally have required a web server and a shared database or pub-sub broker 🎉

Next steps#

Here is what you can try out now:

  • Run the chat app on another computer in the network
  • Restart the app: do you see the chat history?
  • Airplane mode: what happens if you chat while offline?
  • Reconnecting: what happens when you disable airplane mode again?

We hope that you are now starting to experience the power of local-first cooperation and the Actyx Platform. If you are keen to dive a bit deeper, check out the following further resources.

Download the code#

You can download the complete code for this tutorial from GitHub here.

Further resources#

  • Move onto the advanced tutorial to implement a factory use-case
  • Visit our how-to guides to get started immediately
  • Dive into how it works with the Conceptual Guides
Join our Discord chat

Feel free to join our Actyx Developer Chat on Discord. We would love to hear about what you want to build on the Actyx Platform.