Front-End Web & Mobile

Announcing server-side filters GraphQL subscriptions with AWS Amplify

Today, AWS Amplify is launching the ability for you to filter real-time GraphQL subscription events service-side with Amplify CLI version 10.3.1. This gives developers the ability to optimize network traffic by only getting the real-time events for the data they care about. For example, for a live stream broadcasting website, developers can now filter the stream chat messages based to a specific stream.

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 Twitter clone that receives a constant stream of updates. The customer can then provide a text filter to only receive new tweets if a given text is included.

Diagram of server-side subscription filters

What we’ll learn

  • How to create a GraphQL API connected to a DynamoDB database table
  • How to setup real-time subscriptions for the GraphQL API
  • How to dynamically configure server-side subscription filters for a GraphQL API

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

1. Create a new React application

Run the following command to create a new Amplify project called “tweet-storm”:

npx create-react-app tweet-storm
cd tweet-storm

Next, we’ll install some frontend dependencies for the later parts of the demo:

npm i aws-amplify @aws-amplify/ui-react @faker-js/faker @formkit/auto-animate

This is what the dependencies do:

  • aws-amplify – the AWS Amplify JavaScript Library allows you to connect to your AWS app backend. We’ll use this library to make the GraphQL requests and handle real-time subscription events.
  • @aws-amplify/ui-react (optional) – the AWS Amplify React UI library gives developers accessible, themeable, performant React components. We’ll use this to style our app more easily.
  • @faker-js/faker(optional) – FakerJS enables developers to generate fake (but realistic) data for testing and development. We’ll use this to generate fake tweets to simulate a “tweet storm”.
  • @formkit/auto-animate(optional) – AutoAnimate adds smooth transitions to your web app. We’ll use this to make the new tweets animate in as they are generated.

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

Let’s set up your backend with a GraphQL API. First, initialize an Amplify project by running the following command in your Terminal:

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

For the sake of this demo, we can accept all default values and just select “Continue”

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

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. Replace the contents of the schema.graphql file with the following:

type Tweet @model @auth(rules:[{ allow: public }]){
  author: String
  content: String!
}

@model creates a DynamoDB table for you to store all the tweets along with a few auto-generated fields, including:

  • id – A unique identifier for each record. (You can customize this using the @primaryKey directive)
  • createdAt – A timestamp on when the record was created.

@auth configures the authorization rule for your data model. Only for the sake of the demo, we’ll allow create, read, update, and delete operations via a publicly shareable API key.

Now, let’s deploy your app backend by running the following command in your Terminal:

amplify push -y

As part of the deployment, Amplify CLI will also generate assets for the Amplify Libraries to connect to your backend. This includes:

  • aws-exports.js – Configuration information for the Amplify Libraries to connect to the AWS backend.
  • graphql/* files – GraphQL query, mutation, and subscription code that you can pass into the AWS Amplify Libraries to run the GraphQL requests.

3. Connect React app to GraphQL API

First, let’s configure your React app to “be aware” of the Amplify-generated backend and configure the default Amplify UI theme.

Come back to your editor and replace the contents of your index.js file with the following code:

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

Amplify.configure(awsconfig);

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

// 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();

This 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 <ThemeProvider /> theme provider for your <App /> component).

Next, let’s build out the rest of the app by replacing the App.js file with the following content. This sets up an UI for you to simulate new tweets being created every second when you click on “Start Tweet Storm”.

import { useEffect, useState } from 'react'
import { Button, Divider, Flex, TextField, Text } from '@aws-amplify/ui-react'
import { useAutoAnimate } from '@formkit/auto-animate/react'
import { API } from 'aws-amplify'
import { createTweet } from './graphql/mutations'
import { onCreateTweet } from './graphql/subscriptions'
import { faker } from '@faker-js/faker'

function Tweet({ tweet }) {
  const { author, content, createdAt } = tweet
  return (
    <Flex border={"1px solid black"} direction={"column"} padding={8}>
      <Text><b>{author}</b> - <i>{new Date(createdAt).toLocaleString()}</i>:</Text>
      <Text>{content}</Text>
    </Flex>
  )
}

function App() {
  const [tweets, setTweets] = useState([])
  const [textFilter, setTextFilter] = useState()
  const [tweetStormTimer, setTweetStormTimer] = useState()
  const [animationParent] = useAutoAnimate() // Enables smooth animations when new tweets are added

  return (
    <Flex direction="column" padding={8}>
      <Flex>
        <Button onClick={startTweetStorm} disabled={!!tweetStormTimer}>Start tweet storm</Button>
        <Button onClick={stopTweetStorm} disabled={!tweetStormTimer}>Stop tweet storm</Button>
        <TextField label={"Tweet must contain"} onChange={(e) => setTextFilter(e.target.value)} />
      </Flex>
      <Divider />
      <Flex direction={'column'} ref={animationParent}>
        {tweets.map((tweet) => <Tweet key={tweet.id} tweet={tweet} />)}
      </Flex>
    </Flex>
  );
}

export default App;

In order to showcase server-side subscriptions later, we need to first simulate a constant stream of tweets being created. For the sake of this demo, we’re going to simulate it from within the same app but in reality, these events can be created from any client that’s authorized to create tweets to the generated AppSync API.

Add the following code above the return statement of your App component:

  function startTweetStorm() {
    const timer = setInterval(() => {
      if (document.visibilityState !== 'visible') {
        // Prevents superfluous traffic in case
        // demo is running by mistake in the background
        return
      }

      API.graphql({
        query: createTweet,
        variables: {
          input: {
            author: faker.name.fullName(),
            content: faker.hacker.phrase()
          }
        }
      })
    }, 1000)
    setTweetStormTimer(timer)
  }

  function stopTweetStorm() {
    clearInterval(tweetStormTimer)
    setTweetStormTimer()
  }

Lastly, set up a subscription to listen to all newly created tweets (without any specific filters) so we can render them as they come in. To do this, add the following useEffect hook above the event handlers in the App component:

  useEffect(() => {
    const sub = API.graphql({
      query: onCreateTweet
    }).subscribe({
      next: ({ value }) => {
        setTweets((tweets) => [value.data.onCreateTweet, ...tweets])
      }
    })

    return () => sub.unsubscribe()
  }, [textFilter])

We can now start your app by running the following command in your Terminal:

npm start

Try playing around with the interface and start the tweet storm. As you can see, the amount of these simulated tweets can be quite overwhelming. (Note: the filter doesn’t do anything just yet).

Demo of simulated incoming tweets

To verify if you’re on the right track, your code should look something like this:

import { Button, Divider, Flex, TextField, Text } from '@aws-amplify/ui-react'
import { useAutoAnimate } from '@formkit/auto-animate/react'
import { API } from 'aws-amplify'
import { useEffect, useState } from 'react';
import { createTweet } from './graphql/mutations'
import { onCreateTweet } from './graphql/subscriptions'
import { faker } from '@faker-js/faker';

function Tweet({ tweet }) {
  const { author, content, createdAt } = tweet
  return (
    <Flex border={"1px solid black"} direction={"column"} padding={8}>
      <Text><b>{author}</b> - <i>{new Date(createdAt).toLocaleString()}</i>:</Text>
      <Text>{content}</Text>
    </Flex>
  )
}

function App() {
  const [tweets, setTweets] = useState([])
  const [textFilter, setTextFilter] = useState()
  const [tweetStormTimer, setTweetStormTimer] = useState()
  const [animationParent] = useAutoAnimate()

  useEffect(() => {
    const sub = API.graphql({
      query: onCreateTweet
    }).subscribe({
      next: ({ value }) => {
        setTweets((tweets) => [value.data.onCreateTweet, ...tweets])
      }
    })

    return () => sub.unsubscribe()
  }, [textFilter])

  function startTweetStorm() {
    const timer = setInterval(() => {
      if (document.visibilityState !== 'visible') {
        // Prevents superfluous traffic in case
        // demo is running by mistake in the background
        return
      }

      API.graphql({
        query: createTweet,
        variables: {
          input: {
            author: faker.name.fullName(),
            content: faker.hacker.phrase()
          }
        }
      })
    }, 1000)
    setTweetStormTimer(timer)
  }

  function stopTweetStorm() {
    clearInterval(tweetStormTimer)
    setTweetStormTimer()
  }

  return (
    <Flex direction="column" padding={8}>
      <Flex>
        <Button onClick={startTweetStorm} disabled={!!tweetStormTimer}>Start tweet storm</Button>
        <Button onClick={stopTweetStorm} disabled={!tweetStormTimer}>Stop tweet storm</Button>
        <TextField label={"Tweet must contain"} onChange={(e) => {
          setTextFilter(e.target.value)
        }} />
      </Flex>
      <Divider />
      <Flex direction={'column'} ref={animationParent}>
        {tweets.map((tweet) => <Tweet key={tweet.id} tweet={tweet} />)}
      </Flex>
    </Flex>
  );
}

export default App;

4. Set up server-side GraphQL subscription filters

As you can see, the tweets can get quite noisy. With server-side subscriptions filters, you are able to configure additional conditions on when a subscription event should be propagated to the end-customer.

To configure server-side subscription filters, pass in a filter argument into the variables parameter when you set up the GraphQL subscription. Replace the useEffect hook with the following content:

  useEffect(() => {
    let filter
    if (textFilter) {
      filter = {
        content: {
          contains: textFilter
        }
      }
    }

    const sub = API.graphql({
      query: onCreateTweet,
      variables: {
        filter
      }
    }).subscribe({
      next: ({ value }) => {
        setTweets((tweets) => [value.data.onCreateTweet, ...tweets])
      }
    })

    return () => sub.unsubscribe()
  }, [textFilter])

Now, let’s come back to our browser and test out this feature. (Note: in a real-world app, you should debounce the subscription set up, so it doesn’t happen on every keystroke.)

If I specify that a tweet must contain “system” as a word, it enforces the filter service-side. The client only receives “createTweet” events if the content contains the specified filter:

Demo of server-side subscription filter

5. Combine multiple server-side GraphQL subscription filters

GraphQL subscription filters can be also combined with multiple conditions. To do so, modify the filter to add filter conditions to the other fields. For demo purposes, we only want tweets that contain a certain text and are authored by people with “e” in their name:

      filter = {
        content: {
          contains: textFilter
        },
        author: {
          contains: "e"
        }
      }

By default, server-side subscriptions filters are “logically ANDed”; meaning both conditions must be true in order for the event to be sent to the client. Because this filter is much more restrictive, you can see that events are coming through significantly slower now:

Demo of logically-AND'ed subscription filter

You can alternatively, “logically OR“ them by grouping the conditions into a or array element.

      filter = {
        or: [{
          content: {
            contains: textFilter
          },
        }, {
          author: {
            contains: "e"
          }
        }]
      }

When you go back to your browser, you can see significantly more messages arriving because this filter is more permissive than the prior one:

Demo of logically-OR'ed subscription filter

🥳 Success

In this blog post, you’ve created server-side GraphQL subscription filters and learned how to combine multiple filters. Check out our documentation for more details on how to configure server-side subscriptions.

To tear down your Amplify backend and clean-up the generated DynamoDB table and AppSync GraphQL APIs, run amplify delete from your Terminal.

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

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.