Skip to main content

Chroma Chat

In this section we will write the Rell backend for a public chat dapp.


The requirements we set are the following:

  • There is one admin with an amount of tokens automatically assigned (say 1000000).
  • The admin is the first person that registers themselves on the dapp.
  • Any registered user can register a new user and transfer some tokens to them, after having paid 100 tokens to the admin as a fee.
  • Users are identified by their public key.
  • Channels are streams of messages belonging to the same topic, specified by the name of the channel (e.g. "showerthoughts", where you can share thoughts you had in the shower).
  • Registered users can create channels.
  • When a new channel is created, only the creator is within the group. She can add any existing users. This operation costs 1 token.

Entity definition

The structure of it will be:

entity user {
key pubkey;
key username: text;

entity channel {
key name;
admin: user;

entity channel_member {
key channel, member: user;

entity message {
key channel, timestamp;
index posted_by: user;

entity balance {
key user;
mutable amount: integer;

Let's analyse it:


A user can be identified either by its pubkey or by its username. Both pubkey and username are key attributes and are therefore unique.


Channels are identified by the name (which ideally reflects the topic of the channel itself) and the user who created it. Note that two channels cannot have the same name (key) and that a user can be admin of multiple channels.


One message has the text and reference of the user who sent it. Additionally, the channel and timestamp of publication is recorded. Note that key channel, timestamp means that only one message can be sent within a channel at given timestamp (but of course several messages on different channels can be recorded at single timestamp).


This is kind of self explanatory: a user has an amount of tokens. Tokens can be spent (or more in general transferred), for this reason the field is marked as mutable.


Operations are necessary when some data in the database is to be modified.


The module is initialized by performing the init operation. Here, an admin user is created with an account balance of 1000000. We don't want it to be possible to execute the operation a second time.

require( (user@*{} limit 1).size() == 0 ); prevents that.

operation init (founder_pubkey: pubkey) {
require( (user@*{} limit 1).size() == 0 );
val founder = create user (founder_pubkey, "admin");
create balance (founder, 1000000);

The operation receives a public key as input (note that it does not verify that signer of the transaction is the same specified in input field founder_pubkey, meaning you can specify a different public key).

Transfer tokens (Function)

For convenience we create a function to transfer token from one user's balance to another's. We write it because we don't want to duplicate our checks and potentially create bugs.

function transfer_balance(from:user, to:user, amount:integer){
require( balance@{from}.amount >= amount);
update balance@{from} (amount -= amount);
update balance@{to} (amount += amount);

We also add a pay_fee function that is a transfer from one user to the admin account:

function pay_fee (user, deduct_amount: integer) {
if(user.username != 'admin'){
transfer_balance(user, user@{.username == 'admin'}, deduct_amount);

Register a new user

As said, registered users should be allowed to add new users:

operation register_user (
existing_user_pubkey: pubkey,
new_user_pubkey: pubkey,
new_user_username: text,
transfer_amount: integer
) {
require( op_context.is_signer(existing_user_pubkey) );
val existing_user = user@{existing_user_pubkey};

require( transfer_amount > 0 );

val new_user = create user (new_user_pubkey, new_user_username);
pay_fee(existing_user, 100);

create balance (new_user, 0);
transfer_balance(existing_user, new_user, transfer_amount);

Here we:

  • Verify that the signer exists with user@{existing_user_pubkey}, which require exactly one result for the pubkey.
  • Pay the fee of 100 tokens (transfer 100 tokens to 'admin' account).
  • Then create the new user and transfer to them the specified positive amount of tokens.

If at any point in the operation the conditions fail (for example, when the new username is already taken), the whole operation is rolled back and the transaction is rejected. This is why we don't need to check if the signer's balance has registration_cost + transfer_amount tokens beforehand.

Create a new channel

Registered users can create new channels. Given the public key and the name of the channel, we will verify that she is an actual registered user, transfer the fee, create the channel, and add that user as chat member.

operation create_channel ( admin_pubkey: pubkey, name) {
require( op_context.is_signer(admin_pubkey) );
val admin_usr = user@{admin_pubkey};
pay_fee(admin_usr, 100);
val channel = create channel (admin_usr, name);
create channel_member (channel, admin_usr);

Add user to channel

The admin of a channel (the one who created the channel) can add another user after having paid a fee of 1 token.

So we check once again that the signer is the admin_pubkey specified, we have the channel admin pay 1 token, and we add a new user to the channel via channel_member.

operation add_channel_member (admin_pubkey: pubkey, channel_name: name, member_username: text) {
require( op_context.is_signer(admin_pubkey) );
val admin_usr = user@{admin_pubkey};
pay_fee(admin_usr, 1);
val channel = channel@{channel_name, .admin==user@{admin_pubkey}};
create channel_member (channel, member=user@{.username == member_username});

Post a new message

People in a channel will love to share their opinions. They can do so with the post_message operation. The signer (op_context.is_signer(pubkey)) can post a message in the channel (val channel = channel@{channel_name};) if they are a member of the channel (require( channel_member@?{channel, member} );).

After the payment of 1 token fee, we add the new message to the channel:

operation post_message (channel_name: name, pubkey, message: text) {
require( op_context.is_signer(pubkey) );
val channel = channel@{channel_name};
val member = user@{pubkey};
require( channel_member@?{channel, member} );
pay_fee(member, 1);
create message (channel, member, text=message, op_context.last_block_time);


It is useful to write data into a database in a distributed fashion, although writing would be meaningless without the ability to read.

Query all channels where a user is registered

Getting the channels one user is registered into is simple, selecting from channel_member with the given user's public key.

query get_channels(pubkey):list<(name:text, admin: text)> {
return channel_member@*{.member == user@{pubkey}} (name =, admin = .channel.admin.username);

Other simple queries

Likewise we can get the balance from one user.

query get_balance(pubkey) {
return balance@{ user@{ pubkey } }.amount;

Retrieve messages sent in one channel sorted from the oldest to newest (sort .timestamp).

query get_last_messages(channel_name: name):list<(text:text, poster:text, timestamp:timestamp)> {
return message@*{ channel@{channel_name} }
( .text, poster=.posted_by.username, @sort .timestamp );

Run it

  • Browse to
  • Create a new project
  • Enter the above code in the code section (you can copy the full code from here)
  • Click on Start Node (the green "Play" icon)


  • Enter the above code in a new Eclipse Rell project
  • Run it as a "Rell Postchain App" from the run.xml file

Congratulations! You should now have a running node.

Client side

At this stage we should have a running node with your freshly made module.

What about interface it with a classy JS based application?

Well to do it we need the postchain-client npm package

npm i --save postchain-client

Let's open a new script in an editor of your liking and include the postchain client and crypto package.

const pcl = require("postchain-client");
const crypto = require("crypto");

Then we need to declare the address of the REST server (which is ran by the node, default is 7740) and the blockchainRID of the blockchain and the number of sockets (5).

We then get an instance of GTX Client, via gtxClient.createClient and giving the rest object and blockchainRID in input. Last parameters is an empty list of operation (this is needed if you don't use Rell language, in fact, you can also code a module with standard SQL or as a proper kotlin/java module).

// Check the node log on to get node api url.
const nodeApiUrl = "";
const blockchainRID =
"78967baa4768cbcef11c508326ffb13a956689fcb6dc3ba17f4b895cbb1577a3"; // default RID on
const rest = pcl.restClient.createRestClient(nodeApiUrl, blockchainRID, 5);
const gtx = pcl.gtxClient.createClient(
Buffer.from(blockchainRID, "hex"),

If you are using Eclipse IDE, the configs should be:

const nodeApiUrl = "http://localhost:7740/"; //If using another port you can specify it here
const blockchainRID =
"0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF"; //Blockchain RID can be seen in the console window when starting a node

If you are writing your script in the web IDE, you do not have to write configs as it is already included.

Create and send a transaction with the init operation

First thing we probably want is to register and create the admin, we do so calling the init function.

function init(adminPubkey, adminPrivkey) {
const rq = gtx.newTransaction([adminPubkey]);
rq.addOperation("init", adminPubkey);
rq.sign(adminPrivkey, adminPubkey);
return rq.postAndWaitConfirmation();

The first thing we do is to declare a new transaction and that it will be signed by admin private key (we provide the public key, so the node can verify the veracity of transaction).

We add the operation called init and we pass as input argument the admin public key. We then sign the transaction with the private key (we specify the public key in order to correlate which private key refers to which public key in case of multiple signatures).

Finally we send the transaction to the node via the method postAndWaitconfirmation which returns a promise and resolves once it is confirmed.

Given the following keypair, we can create the admin.

const adminPUB = Buffer.from(
const adminPRIV = Buffer.from(

init(adminPUB, adminPRIV);

In your own project, you might want to generate the keypair using pcl.util.makeKeyPair() instead:

const user = pcl.util.makeKeyPair();
const { pubKey, privKey } = user;

Create other operations

We can also create a new channel, post a message, invite a user to dapp, invite a user in a channel.

function createChannel(admin, channelName) {
const pubKey = pcl.util.toBuffer(admin.pubKey);
const privKey = pcl.util.toBuffer(admin.privKey);
const rq = gtx.newTransaction([pubKey]);
rq.addOperation("create_channel", pubKey, channelName);
rq.sign(privKey, pubKey);
return rq.postAndWaitConfirmation();

function postMessage(user, channelName, message) {
const pubKey = pcl.util.toBuffer(user.pubKey);
const privKey = pcl.util.toBuffer(user.privKey);
const rq = gtx.newTransaction([pubKey]);
rq.addOperation("nop", crypto.randomBytes(32));
rq.addOperation("post_message", channelName, pubKey, message);
rq.sign(privKey, pubKey);
return rq.postAndWaitConfirmation();

function inviteUser(existingUser, newUserPubKey, startAmount) {
const pubKey = pcl.util.toBuffer(existingUser.pubKey);
const privKey = pcl.util.toBuffer(existingUser.privKey);
const rq = gtx.newTransaction([pubKey]);
rq.sign(privKey, pubKey);
return rq.postAndWaitConfirmation();

function inviteUserToChat(existingUser, channel, newUserPubKey) {
const pubKey = pcl.util.toBuffer(existingUser.pubKey);
const privKey = pcl.util.toBuffer(existingUser.privKey);
const rq = gtx.newTransaction([pubKey]);
rq.sign(privKey, pubKey);
return rq.postAndWaitConfirmation();

Although there is really nothing critical in these functions, there are a few things worth noting:

  • We expect public and private keys in hex format, and we convert them to Buffer with pcl.util.toBuffer(admin.pubKey);.
  • In order to protect the system from replay attacks, the blockchain does not accept transactions which hash is equal to an already existing transaction. This means that an user is not allowed to write the same message twice in a channel since if at day one he writes "hello" the transaction will be something like rq.addOperation("post_message", the_channel, user_pub, "hello");, when he will write 'hello' a second time the transaction will be the same and therefore rejected. To solve this problem, we add a "nop" operation with some random bytes via rq.addOperation("nop", crypto.randomBytes(32));, and create a different transaction hash.

It is very important to remember this limitation imposed upon transactions. If your transaction is rejected with no obvious reason, chances are high that it is missing a "nop" operation.

Querying the blockchain from the client side

Previously we wrote the queries on blockchain side. Now we need to query from the dapp. To do so we use the previously mentioned postchain-client package.

// Rell query, reported here for easy look up
// query get_balance(user_pubkey: text) {
// return balance@{user@{byte_array(user_pubkey)}}.amount;
// }

function getBalance(user) {
return gtx.query("get_balance", {
user_pubkey: user.pubKey,

As you can see everything is contained into gtx.query: the first argument is the query name in the rell module, and the second argument is the name of the expected attribute in the query itself wrapped in an object. The name of the object is the one specified in module and the value, of course, the value we want to send. Please note that buffer values must before be converted into hexadecimal strings.

Other queries:

function getChannels(user) {
return gtx.query("get_channels", {
user_pubkey: user.pubKey,

function getMessages(channel) {
return gtx.query("get_last_messages", { channel_name: channel });


At this point, we have created a Rell backend for the public chat, and a javascript client to communicate with it.

We encourage you to extend this sample in anyway you like, by for example adding a user interface, or maybe by adding a "transfer" operation to send tokens to another user?

Or, if you are eager to see the application in its running state, we have implemented a simple UI for it at