Building Chatt - A Real-time Multi-user GraphQL Chat App

Nader Dabit - Feb 6 '19 - - Dev Community

This repo and project is now archived

Chatt

One of the most popular use-cases of GraphQL subscriptions is building applications that enable real-time communications (i.e. messaging apps).

One of the more difficult things to do is have this real-time functionality work with multiple users & multiple channels as the data model begins to be somewhat complex & scalability issues begin to come into play when you have a large number of connected clients.

I recently built & released an open-source app, Chatt, that implements this real-time functionality with multiple users & the ability to subscribe to individual channels (chats) based on whether you are in the conversation.

When building something like this, there are two main pieces you have to get set up:

  1. User management
  2. The API

Typically, building both of these from scratch is a huge undertaking to say the least, & building them both to be scalable & secure could take months.

Thankfully today we have services like Auth0, Firebase, Okta & AppSync that allow us to spin up managed services to handle these types of workloads.

My app is using AWS AppSync for the GraphQL API & AWS Amplify to create the user management service. The app is built to work with these services but they could pretty easily be replaced with another back end or authentication provider.

To deploy this app & back end yourself, check out the instructions in the README

The Code

Let's take a quick look at some of the code. The first thing we'll look at is the base schema:

type User {
  id: ID!
  username: String!
  conversations(filter: ModelConvoLinkFilterInput, sortDirection: ModelSortDirection, limit: Int, nextToken: String): ModelConvoLinkConnection
  messages(filter: ModelMessageFilterInput, sortDirection: ModelSortDirection, limit: Int, nextToken: String): ModelMessageConnection
  createdAt: String
  updatedAt: String
}

type Conversation {
  id: ID!
  messages(filter: ModelMessageFilterInput, sortDirection: ModelSortDirection, limit: Int, nextToken: String): ModelMessageConnection
  associated(filter: ModelConvoLinkFilterInput, sortDirection: ModelSortDirection, limit: Int, nextToken: String): ModelConvoLinkConnection
  name: String!
  members: [String!]!
  createdAt: String
  updatedAt: String
}

type Message {
  id: ID!
  author: User
  authorId: String
  content: String!
  conversation: Conversation!
  messageConversationId: ID!
  createdAt: String
  updatedAt: String
}

type ConvoLink {
  id: ID!
  user: User!
  convoLinkUserId: ID
  conversation: Conversation!
  convoLinkConversationId: ID!
  createdAt: String
  updatedAt: String
}
Enter fullscreen mode Exit fullscreen mode

There are three main base GraphQL types: User, Conversation, & Message. There is also a ConvoLink type that provides an association between the conversation & the user.

The operations & resolvers for these types can be viewed in more detail here.

The next thing we'll look at is the GraphQL operations that we'll be using on the client (queries, subscriptions, & mutations) because they give a good view into how the app interacts with the API.

Mutations

// This creates a new user, storing their username.
// Even though the authentication service will be handling the user management, we will also need some association with the user in the database.
const createUser = `
  mutation($username: String!) {
    createUser(input: {
      username: $username
    }) {
      id username createdAt
    }
  }
`

// This creates a new message.
// The association between the message & the conversation is made with the __messageConversationId__.
const createMessage = `mutation CreateMessage(
    $createdAt: String, $id: ID, $authorId: String, $content: String!, $messageConversationId: ID!
  ) {
  createMessage(input: {
    createdAt: $createdAt, id: $id, content: $content, messageConversationId: $messageConversationId, authorId: $authorId
  }) {
    id
    content
    authorId
    messageConversationId
    createdAt
  }
}
`;

// This creates a new conversation.
// We store the members that are involved with the conversation in the members array.
const createConvo = `mutation CreateConvo($name: String!, $members: [String!]!) {
  createConvo(input: {
    name: $name, members: $members
  }) {
    id
    name
    members
  }
}
`;

// This makes the association between the conversations & the users.
const createConvoLink = `mutation CreateConvoLink(
    $convoLinkConversationId: ID!, $convoLinkUserId: ID
  ) {
  createConvoLink(input: {
    convoLinkConversationId: $convoLinkConversationId, convoLinkUserId: $convoLinkUserId
  }) {
    id
    convoLinkUserId
    convoLinkConversationId
    conversation {
      id
      name
    }
  }
}
`;
Enter fullscreen mode Exit fullscreen mode

Using these four operations, we can effectively create all of the data we will need for our app to function. After we've created the data, how to we query for it? Let's have a look.

Queries

// Fetches a single user.
const getUser = `
  query getUser($id: ID!) {
    getUser(id: $id) {
      id
      username
    }
  }
`

// Fetches a single user as well as all of their conversations
const getUserAndConversations = `
  query getUserAndConversations($id:ID!) {
    getUser(id:$id) {
      id
      username
      conversations(limit: 100) {
        items {
          id
          conversation {
            id
            name
          }
        }
      }
    }
  }
`

// gets a single conversation based on ID
const getConvo = `
  query getConvo($id: ID!) {
    getConvo(id:$id) {
      id
      name
      members
      messages(limit: 100) {
        items {
          id
          content
          authorId
          messageConversationId
          createdAt
        }
      }
      createdAt
      updatedAt
    }
  }
`

// lists all of the users in the app
const listUsers = `
  query listUsers {
    listUsers {
      items {
        id
        username
        createdAt
      }
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

For the real time piece, we have 2 subscriptions.

Subscriptions

// When a new message is created, send an update to the client with the id, content, authorId, createdAt & messageConversationId fields
const onCreateMessage = `
  subscription onCreateMessage($messageConversationId: ID!) {
    onCreateMessage(messageConversationId: $messageConversationId) {
      id
      content
      authorId
      messageConversationId
      createdAt
    }
  }
`

// When a new user is created, send an update to the client with the id, username, & createdAt fields
const onCreateUser = `subscription OnCreateUser {
  onCreateUser {
    id
    username
    createdAt
  }
}
`;
Enter fullscreen mode Exit fullscreen mode

State management

There isn't much actual state management that goes on outside of the Apollo / AppSync SDK. The only thing I have implemented outside of that is a way to access the user data in a synchronous way by storing it in MobX. In the future, I'd like to replace this with Context or possibly even merging in with Apollo as well.

Offline

As far as offline functionality is concerned, since we're using the AWS AppSync JS SDK for most of it, there is nothing else we have to do other than provide the right optimistic updates.

The AppSync JS SDK leverages the existing Apollo cache to handle offline scenarios & queue up any operations that happen offline. When the user comes back online, the updates are sent to the server in the order in which they were created.

Conclusion

I learned a lot about working with subscriptions when building this app, & will be adding additional functionality like the aforementioned state management being completely handled by the AppSync SDK among other things.

To learn more about this philosophy of leveraging managed services & APIs to build robust applications, check out my post Full-Stack Development in the Era of Serverless Computing.

My Name is Nader Dabit. I am a Developer Advocate at AWS Mobile working with projects like AWS AppSync and AWS Amplify. I also specialize in cross-platform application development.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .