Front-End Web & Mobile

Build real-time multi-user experiences using GraphQL on AWS Amplify

Today, AWS Amplify announces new real-time authorization capabilities enabling developers to build collaboration experiences with only a few lines of code. This features enables developers to share data between users by simply appending an “array” of data owners. Update to Amplify CLI version 10.3.1 and above and deploy your GraphQL API to enable this feature.

AWS Amplify is the fastest and easiest way to build cloud-powered mobile and web apps on AWS. Amplify comprises a set of tools and services that enables frontend web and mobile developers to leverage the power of AWS services to build innovative and feature-rich applications. The AWS Amplify CLI is a command line toolchain that helps frontend developers create app backends in the cloud.

In this tutorial, we’ll build a share-able todo list app, where customers can create todo and share it with other users on the app. Check out the demo of what we’ll build below: User A (chris) creates a Todo and shares it with User B (rene). The shared todo shows up on User B (rene)’s device in real-time!

Real-time multi-user GraphQL experience demo

What we’ll learn

  • How to create a GraphQL API + database connections with multi-owner authorization
  • How to build an authenticated React app to talk to the GraphQL API
  • How to setup real-time subscriptions for added, updated, and deleted data records

Pre-requisites

  • Install the latest Amplify CLI; version 10.3.1 and above required.
    • Open terminal and run npm i -g @aws-amplify/cli
  • Amplify CLI is already configured

Create a new React application

Run the following command to create a new Amplify project called “share-todo” or if you already have an existing Amplify project skip to the next section.

npx create-react-app share-todo
cd share-todo

Set up your app backend (GraphQL API) with Amplify CLI

Initialize an Amplify project by running:

amplify init -y

Let’s add a new GraphQL API to sync your app data to the cloud. Run the following command:

amplify add api

Important: Use “Up-arrow” key to select a different “Authorization modes” > “Amazon Cognito User Pool”. This allows us to build a user sign-up and sign-in flow.

? Here is the GraphQL API that we will create. Select a setting to edit or continue
  Name: sharetodo
❯ Authorization modes: API key (default, expiration time: 7 days from now)
  Conflict detection (required for DataStore): Disabled
  Continue

This will ultimately lead you down a path to setup the Amplify “auth” category. For the sake of this demo, you can accept all default options from then on.

? Choose the default authorization type for the API Amazon 
> Cognito User Pool
Use a Cognito user pool configured as a part of this project.
? Configure additional auth types?
> No
? Here is the GraphQL API that we will create. Select a setting to edit or continue
> Continue
? Choose a schema template:
> Single object with fields (e.g., “Todo” with ID, name, description)

Edit your schema at /Users/renbran/Projects/cicdtest/amplify/backend/api/cicdtest/schema.graphql or place .graphql files in a directory at /Users/renbran/Projects/cicdtest/amplify/backend/api/cicdtest/schema
✔ Do you want to edit the schema now? (Y/n)
> yes

Next, we’ll need to edit your schema to define your app data backend. The Amplify CLI should’ve opened up the new GraphQL API schema file now.

Amplify provides “directives” such as @model and @auth to give you a zero-effort way to setup your data backend infrastructure based on your GraphQL API.

type Todo
  @model # Creates a DynamoDB table
  @auth(rules: [{ allow: owner, ownerField: "owners"}]) # Sets up owner-based authorization
{ 
  id: ID!
  content: String
  owners: [String] # Use a String array type to configure multi-owner authorization
}

The GraphQL schema above configures a DynamoDB Table to store your to-dos and configures multi-owner authorization.

  • @model – Use this directive on a GraphQL type to automatically create a backend database table for the type with fields translated as columns/attributes of the database table.
  • @auth – Allows you to configure authorization rules for your database table and fields. In this demo, we’ll only use “owner-based” authorization, which allows a record owner to create, read, update, and delete their own records.

Good to know: By default, to configure owner-based authorization you only need to write { allow: owner } as the rule. In this demo though, we want to allow a data record (a todo) to be owned by multiple users. Therefore, we’re going to override where the record owners are stored (ownerField). If the ownerField is a String array, then Amplify CLI will automatically configure the backend infrastructure to accommodate multi-owner authorization.

Now, let’s deploy your app backend by running:

amplify push

Make sure to accept all the default values for the CLI prompts. While deployment is in progress, you can continue onto the next step with a new Terminal window.

Set up your frontend dependencies

Let’s install the Amplify libraries and Amplify UI for your React app to get started. Note: We’re using Amplify UI, which is optional for this demo but makes the app look great instantly with little configuration.

Run the following command in your Terminal from your React project root folder:

npm install aws-amplify @aws-amplify/ui-react

Next, we’ll need to configure your React app to “be aware” of the Amplify-generated backend and configure a basic Amplify UI theme to make the UI components look great.

Replace your index.js file with the following code, which imports the Amplify libraries and connects them to the backend configuration (stored in aws-exports.js). In addition, it’s configuring the default Amplify UI theme (the imported styles.css file and <AmplifyProvider /> theme provider for your <App /> component).

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { AmplifyProvider } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import { Amplify } from 'aws-amplify'
import awsconfig from './aws-exports'

Amplify.configure(awsconfig)

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <AmplifyProvider>
    <App />
  </AmplifyProvider>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Now your React app is ready to be “amplify-ed”!

Build a login in screen (in literally 2 lines of code; I promise)

Go to your App.js file and import the withAuthenticator higher-order component from the Amplify UI library and wrap your App component with it.

Add the following import statement to your App.js file:

import { withAuthenticator } from '@aws-amplify/ui-react'

Then replace the export statement of your App component with the withAuthenticator HOC. (This is on the very bottom of your App.js file)

export default withAuthenticator(App);

Now, let’s try out the changes. Go to your Terminal and run:

npm start

🚀 In your browser, you should see a login experience now. Try signing up a at least two users. Recommended: Open https://localhost:3000/ in an Incognito window to set up the second user.

Demo of Authenticator and signing up a user

Display user login information and signOut action

Now that we have the ability to sign-up and sign-in users, let’s also build a quick mechanism to show who’s logged and give them the ability to log out. The withAuthenticator component automatically passes a user and signOut prop that into the wrapped component.

Edit your App.js file to display your user’s username and gives them the ability to log out. Replace the contents of App.js with the following code:

import { withAuthenticator, Text, Flex, View, Badge, Button } from '@aws-amplify/ui-react'

function App({ user, signOut }) {
  return (
    <Flex direction={"column"} padding={8}>
      <Text>Logged in as <b>{user.username}</b> <Button variation='link' onClick={signOut}>Sign out</Button></Text>
    </Flex>
  );
}

export default withAuthenticator(App);

Create and display data records (todos) with GraphQL

Amplify CLI automatically generates GraphQL queries, mutations, and subscriptions into your src/graphql folder. This gives you a quick way to call these GraphQL APIs without writing out entire GraphQL statements manually.

Let’s first add a button to the app to create a new todo. Import the Amplify library’s API category and createTodo GraphQL mutation in the App.js file:

import { API, graphqlOperation } from 'aws-amplify'
import { createTodo } from './graphql/mutations'

Below your user login information, let’s add a new button to create a todo. For this demo’s sake, we’re going to use the trusty window.prompt to fetch the contents of my todo app.

<Button onClick={() => {
    API.graphql(graphqlOperation(createTodo, {
        input: {
        content: window.prompt('content?'),
        }
    }))
}}>Add todo</Button>

Important: Remember when you configured “Amazon Cognito User Pool” earlier as the authorization mode earlier? Amplify API will automatically now add your Amazon Cognito user’s username to the record as an “owner” because Amazon Cognito User Pool is the default authorization mode AND owner-based authorization is configured.

Next, let’s display your todos. Import the listTodos GraphQL queries from your src/graphql folder and the required React hooks:

import { listTodos } from './graphql/queries'
import { useState, useEffect } from 'react'

Then, we need to fetch these todos when the App renders and display them. Create a state to store the todos and a useEffect to fetch the todos.

const [todos, setTodos] = useState([])

useEffect(() => {
    const fetchTodos = async () => {
        const result = await API.graphql(graphqlOperation(listTodos))
        setTodos(result.data.listTodos.items)
    }
    
    fetchTodos()
}, [])

Next, display the fetched todos in the return statement of the App component. Add the following React code right below the “Add Todo” button component code.

{todos.map(todo => <Flex direction="column" border="1px solid black" padding={8} key={todo.id}>
    <Text fontWeight={'bold'}>{todo.content}</Text>
    <View>👨‍👩‍👧‍👦 {todo.owners.map(owner => <Badge margin={4}>{owner}</Badge>)}</View>
</Flex>)}

You should now be able to add new todos and upon refresh of the app see them listed. Try it out in your browser and go ahead and add a few to-dos and then refresh the page.

Demo of GraphQL create operation working

Now that we’ve got a few todos added, we also need the ability to delete them. Import the the deleteTodo GraphQL mutation and let’s add a button to delete a todo within each todo.

import { createTodo, deleteTodo } from './graphql/mutations'

Now let’s add a delete button below the View component that displays all the to-do owners. (We’ll get to the “owners“ in a bit!)

<Button onClick={async () => {
    API.graphql(graphqlOperation(deleteTodo, {
        input: { id: todo.id }
    }))
}}>Delete</Button>

Your App.js file should look something like this:

import { withAuthenticator, Text, Flex, View, Button, Badge } from '@aws-amplify/ui-react'
import { useEffect, useState } from 'react';
import { API, graphqlOperation } from 'aws-amplify'
import { createTodo, deleteTodo } from './graphql/mutations'
import { listTodos } from './graphql/queries'

function App({ user, signOut }) {
  const [todos, setTodos] = useState([])

  useEffect(() => {
    const fetchTodos = async () => {
      const result = await API.graphql(graphqlOperation(listTodos))
      setTodos(result.data.listTodos.items)
    }

    fetchTodos()
  }, [])

  return (
    <Flex direction={"column"} padding={8}>
      <Text>Logged in as <b>{user.username}</b> <Button variation='link' onClick={signOut}>Sign out</Button></Text>
      <Button onClick={() => {
        API.graphql(graphqlOperation(createTodo, {
          input: {
            content: window.prompt('content?'),
          }
        }))
      }}>Add todo</Button>
      {todos.map(todo => <Flex direction="column" border="1px solid black" padding={8} key={todo.id}>
        <Text fontWeight={'bold'}>{todo.content}</Text>
        <View>👨‍👩‍👧‍👦 {todo.owners.map(owner => <Badge margin={4}>{owner}</Badge>)}</View>
        <Button onClick={async () => {
            API.graphql(graphqlOperation(deleteTodo, {
              input: { id: todo.id }
            }))
          }}>Delete</Button>
      </Flex>)}
    </Flex>
  );
}

export default withAuthenticator(App);

Demo of GraphQL deletion working

Fetch real-time updates for records (todos) with GraphQL

It’s good that we’ve got create and read working but in reality we really just want real-time updates to be automatically reflected after they occur. If I create a new todo, it should instantly show up in my app and similarly if I delete a todo, it should also instantly delete it for me.

GraphQL provides a mechanism called “subscriptions” to stream in real-time updates. Amplify automatically creates subscriptions for record creation, updates, and deletions. In your app code, you can then “apply” those updates to your existing/fetched data.

Let’s import the subscription statements for your GraphQL API into App.js:

import { onCreateTodo, onUpdateTodo, onDeleteTodo } from './graphql/subscriptions';

Next, we need to “subscribe” to those events and then apply the event inputs onto the fetched todos. We’ll need to handle each case: create, update, and delete separately because they have different implications for the fetched todos.

Let’s add all the subscription logic to your useEffect hook below your fetchTodos() function call.

  useEffect(() => {
    const fetchTodos = async () => {
      const result = await API.graphql(graphqlOperation(listTodos))
      setTodos(result.data.listTodos.items)
    }

    fetchTodos()
    const createSub = API.graphql(graphqlOperation(onCreateTodo)).subscribe({
      next: ({ value }) => { setTodos((todos) => [...todos, value.data.onCreateTodo]) }
    })

    const updateSub = API.graphql(graphqlOperation(onUpdateTodo)).subscribe({
      next: ({ value }) => {
        setTodos(todos => {
          const toUpdateIndex = todos.findIndex(item => item.id === value.data.onUpdateTodo.id)
          if (toUpdateIndex === - 1) { // If the todo doesn't exist, treat it like an "add"
            return [...todos, value.data.onUpdateTodo]
          }
          return [...todos.slice(0, toUpdateIndex), value.data.onUpdateTodo, ...todos.slice(toUpdateIndex + 1)]
        })
      }
    })

    const deleteSub = API.graphql(graphqlOperation(onDeleteTodo)).subscribe({
      next: ({ value }) => {
        setTodos(todos => {
          const toDeleteIndex = todos.findIndex(item => item.id === value.data.onDeleteTodo.id)
          return [...todos.slice(0, toDeleteIndex), ...todos.slice(toDeleteIndex + 1)]
        })
      }
    })

    return () => {
      createSub.unsubscribe()
      updateSub.unsubscribe()
      deleteSub.unsubscribe()
    }
  }, [])

Make sure to return the unsubscribe() calls in the useEffect hook to clean-up the GraphQL subscriptions.

Important: When setting a React state from within a subscription make sure to use the “function” pattern of setState, where you can fetch the previous state. setState((prevState) => newState) The subscription is set up in an useEffect hook, if you just reference todos without this function-based pattern, todos would always return an empty array.

Now, let’s try out your app locally. Every time you create or delete a todo, the changes are reflected in real-time.

Demo of real-time subscriptions with GraphQL

Make data records (todos) shareable with GraphQL

Now to the best part of the demo! Let’s make the to-dos shareable! Remember the “owners” fields that we set up for the Todo model in the GraphQL schema? Well we can append that with any user’s username to make them owners of a record as well!

Let’s import the updateTodo mutation:

import { createTodo, deleteTodo, updateTodo } from './graphql/mutations'

Now, let’s add a button (right before the delete button) to share the todo with other users.

<Button onClick={async () => {
    API.graphql(graphqlOperation(updateTodo, {
        input: {
        id: todo.id,
        owners: [...todo.owners, window.prompt('Share with whom?')]
        }
    }))
}}>Share ➕</Button>

That’s it! Let’s try it now! Open two browser tabs (one in incognito) and login with two separate user accounts. Then create a new todo and share it with the other user by just typing their username after clicking on the “Share button”.

Your App.js file should look something like the below in case you need an end-to-end reference:

import { withAuthenticator, Text, Flex, View, Button, Badge } from '@aws-amplify/ui-react'
import { useEffect, useState } from 'react';
import { API, graphqlOperation } from 'aws-amplify'
import { createTodo, deleteTodo, updateTodo } from './graphql/mutations'
import { onCreateTodo, onUpdateTodo, onDeleteTodo } from './graphql/subscriptions';
import { listTodos } from './graphql/queries'

function App({ user, signOut }) {
  const [todos, setTodos] = useState([])

  useEffect(() => {
    const subscriptionFilter = { filter: { } }

    const fetchTodos = async () => {
      const result = await API.graphql(graphqlOperation(listTodos))
      setTodos(result.data.listTodos.items)
    }

    fetchTodos()
    const createSub = API.graphql(graphqlOperation(onCreateTodo, subscriptionFilter)).subscribe({
      next: ({ value }) => { setTodos((todos) => [...todos, value.data.onCreateTodo]) }
    })

    const updateSub = API.graphql(graphqlOperation(onUpdateTodo, subscriptionFilter)).subscribe({
      next: ({ value }) => {
        setTodos(todos => {
          const toUpdateIndex = todos.findIndex(item => item.id === value.data.onUpdateTodo.id)
          if (toUpdateIndex === - 1) { // If the todo doesn't exist, treat it like an "add"
            return [...todos, value.data.onUpdateTodo]
          }
          return [...todos.slice(0, toUpdateIndex), value.data.onUpdateTodo, ...todos.slice(toUpdateIndex + 1)]
        })
      }
    })

    const deleteSub = API.graphql(graphqlOperation(onDeleteTodo, subscriptionFilter)).subscribe({
      next: ({ value }) => {
        setTodos(todos => {
          const toDeleteIndex = todos.findIndex(item => item.id === value.data.onDeleteTodo.id)
          return [...todos.slice(0, toDeleteIndex), ...todos.slice(toDeleteIndex + 1)]
        })
      }
    })

    return () => {
      createSub.unsubscribe()
      updateSub.unsubscribe()
      deleteSub.unsubscribe()
    }
  }, [])

  return (
    <Flex direction={"column"} padding={8}>
      <Text>Logged in as <b>{user.username}</b> <Button variation='link' onClick={signOut}>Sign out</Button></Text>
      <Button onClick={() => {
        API.graphql(graphqlOperation(createTodo, {
          input: {
            content: window.prompt('content?'),
          }
        }))
      }}>Add todo</Button>
      {todos.map(todo => <Flex direction="column" border="1px solid black" padding={8} key={todo.id}>
        <Text fontWeight={'bold'}>{todo.content}</Text>
        <View>👨‍👩‍👧‍👦 {todo.owners.map(owner => <Badge margin={4}>{owner}</Badge>)}</View>
        <Flex>
          <Button onClick={async () => {
            API.graphql(graphqlOperation(updateTodo, {
              input: {
                id: todo.id,
                owners: [...todo.owners, window.prompt('Share with whom?')]
              }
            }))
          }}>Share ➕</Button>
          <Button onClick={async () => {
            API.graphql(graphqlOperation(deleteTodo, {
              input: { id: todo.id }
            }))
          }}>Delete</Button>
        </Flex>
      </Flex>)}
    </Flex>
  );
}

export default withAuthenticator(App);

🥳 Success! Your app data is shareable and is real-time!

Real-time multi-user GraphQL experience demo

This demo just scratches the surface of what’s possible with AWS Amplify’s GraphQL API category. Multi-owner authorization is our latest addition to our authorization rules. Amplify today already supports a range of authorization rules including public, any signed-in user, custom (using Lambda), and group authorization.

Here are some other cool features you’ll want to check out:

Next on our roadmap to enhance the real-time GraphQL experience:

  • Enable server-side subscription filters
  • Enable real-time updates for dynamic group authorization

As always, feel free to reach out to the Amplify team via GitHub or join our Discord community. Follow @AWSAmplify on Twitter to get the latest updates on feature launches, DX enhancements, and other announcements.