Building real-time chat apps with GraphQL streaming subscriptions

Chat applications on the web or mobile are very common today. But, building a chat app usually  requires a tremendous amount of effort. On the one hand, you have many infrastructure concerns like requiring a communications protocol, websocket management, a message queue, persistence layer and on the other hand, you need to bake in custom logic to handle the security, performance and reliability of your messages.

With Hasura, you can create chat apps INSTANTLY straight from your data models without the need of any other infrastructure using streaming subscriptions. The advantages of using streaming subscriptions on Hasura are:

  • No additional infrastructure requirements apart from Hasura and Postgres. Works with any GraphQL client.
  • Model-level authz that enables sending right events to right clients based on session and data properties.
  • Scale reads the way you would scale Postgres reads, and scale writes the way you would scale Postgres ingestion.
  • Easily scale Hasura to handle horizontal scaling of websocket clients. Benchmarked to handle 1M concurrent GraphQL clients streaming from 10000 channels with 1M events ingested every minute, on a single instance of Postgres RDS.

Let’s look at how streaming subscription works with a sample app. This is what our architecture will look like:

Architecture of Realtime Streaming Chat App
Architecture of Realtime Streaming Chat App

Create Data Models for Chat App

The first step is to create data models that are required for the chat application. Let’s go with a simple chat app where there are some channels and some users. A user needs to be in a particular channel to send and get messages in that channel.

For this, we are creating two data models:

  • messages: This stores the chat messages.
Add Messages Table
Add Messages Table
  • channel_users: This stores the mapping of channels and users. This is what we will use to model the permissions of our app.

We can also create a relationship between messages and channel_users using channel_id as the linking field. We can do this either via foreign keys or manually - we will use manual relationships below and call this relationship recipients.

Add a monotonic id to the messages model

Our messages table needs a monotonic id to identify the order of messages. Why? Streaming data depends on having a “pointer” or cursor to the last processed value. The next batch that is sent to the client would be based on moving ahead on the cursor. If we do not have a monotonic id then we may process a higher id before a lower id and miss streaming some event.

Achieving monotonic ids is tricky to achieve in Postgres because of complex concurrency control. Even in serializable transaction mode, it is not necessary that two transactions that commit in the order of t1 then t2, will necessarily have ids that also follow the same order - serializability only means that there is some serial order not necessarily the commit order. Hence, we need to make an id that is exactly monotonic with the commit timestamp. We can do this in Postgres via advisory locks. Here is a trigger that you can add (via Run SQL in console) to your table so that it generates monotonic ids:

CREATE sequence serial_chats;

CREATE FUNCTION add_monotonic_id()
RETURNS trigger AS $$
DECLARE
nextval bigint;
BEGIN
  PERFORM pg_advisory_xact_lock(1);
  select nextval('serial_chats') into nextval;
  NEW.id := nextval;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER add_monotonic_id_trigger
BEFORE INSERT ON messages
FOR EACH ROW
  EXECUTE FUNCTION add_monotonic_id();

There are more ways to model a monotonic id and depending on your application you may choose to use a different mechanism. One common performance improvement is to take the advisory lock on a partition value instead of a static value like above. This will work only if you need monotonicity over a partition rather than the entire table.

Add permission rules to ensure messages can only be read by channel members

We need to ensure that users can only read and write to channels that they are members of. We can add Hasura permission to the channels model by creating a role and then adding a row filter that checks that the recipients relationship must have the x-hasura-user-id from the incoming query.

Setup Permissions for messages table
Setup Permissions for messages tablex

Trying out streaming subscriptions

With the data models and permissions now configured, we can try out a few queries to see that everything is working properly.

In one tab, open a streaming subscription with the following query and role user and some x-hasura-user-id:

subscription {
  messages_stream(cursor: {initial_value: {id: 1} }, batch_size: 5) {
    id
    message
    author_id
  }
}

In another tab, let's insert some messages for various channels. You can now see that the stream subscription only returns messages for the channels that the user-id is authorized for.

Checkout the source code for the above data model here.

Try out the Full Sample App with Live Demo

We have built out a realtime chat app that uses the Streaming Subscriptions feature in Hasura.

It looks something like this:

Realtime Chat App with Streaming Subscriptions Live Demo
Realtime Chat App with Streaming Subscriptions Live Demo

What the app does is, it fetches the last 10 messages from the messages table and then streams all new messages using the newly added Streaming API.

A GraphQL mutation keeps running in the background to populate online users. A typing indicator is shown on the UI for which a GraphQL mutation is run and a subscription is run to fetch the latest user typing.

The data model for this uses user and message table.

Let us know in comments what other use cases you would like to see with Streaming Subscriptions. We have more example apps in the pipeline which covers both the frontend and backend parts of the app for building realtime apps.

Blog
04 Oct, 2022
Email
Subscribe to stay up-to-date on all things Hasura. One newsletter, once a month.
Loading...
v3-pattern
Accelerate development and data access with radically reduced complexity.