A guide to AWS Amplify subscriptions: not even half of what you need to know

Mauro BosettiMauro Bosetti
6 min read

I've been working with AWS Amplify for quite some time now, and one of the features I struggled to wrap my head around the most has been subscriptions. In this post, I'm going to define what subscriptions are, how they work, and the aspect that confused me the most: how to test them.

But first, let's quickly go over some Amplify concepts and features. If you're familiar with this service, feel free to skip to the subscription-specific content.

What is Amplify?

AWS Amplify is a service that allows developers to quickly spin up full-stack environments without having to manually define and orchestrate a dozen different infrastructure pieces. It has allowed us to start working on product features incredibly fast, while solving complex problems such as data access and storage, user authentication, serverless functions, and automated deployments. With Amplify, the days when even the smallest experiment implied writing loads of Terraform code and state lock storage and CI/CD workflows had to be manually created to get a review app going are gone.

Data access, what we care about the most in this article, is really easy to do. We first need to define data models in a schema file:

const schema = a
    .schema({
        Character: a
            .model({
                name: a.string().required(),
                healthPoints: a.integer().required()
            })
    })

and then access the data from the frontend like this:

const client = generateClient<Schema>();

client.models.Character.list();

How does it work?

To achieve such a level of simplicity, we necessarily have to delegate implementation decisions to AWS. Behind the scenes, the list call turns into a GraphQL query, to be resolved by AWS AppSync, Amazon's own GraphQL service. Each model has its own DynamoDB table, which AppSync uses as a data source to resolve the query.

What are subscriptions then?

Subscriptions are a neat feature that can be used to, you guessed it, subscribe to real-time data updates. Instead of querying multiple times to poll for new data, we can establish a live connection with the GraphQL server where we will receive any new update to an entity. This is what it looks like in the frontend:

client.models.Character.observeQuery().subscribe({
    next: (updatedList) => setCharacters([...updatedList.items])
})

Every time a new character is created or an existing character is updated or is removed, the function passed in next will be called with the updated list.

Client-server communication-wise, this happens by using a feature defined by the GraphQL protocol: subscriptions (maybe add a link here). A webhook connection is established between the client and AppSync, through which an initial snapshot of the data is sent, and subsequent updates are received. Amplify handles syncing the newly received data with the current state in the frontend.

Testing the updates

Once we have a subscription set up in our frontend, such as a small widget that displays the health of our character in real time, a logical next step would be to change the data in the backend to see it reflected live. That is not a hard thing to do, if we know how. But, if we change the data from the wrong place, our subscription will not get triggered, and I spent quite a few hours debugging front end components pointlessly.

Here are some ways we might be tempted to test our subscription, and the issues with those approaches:

DynamoDB πŸ™…

The easiest way to test the subscription would be to go to the Characters DynamoDB table, and change the healthPoints attribute value manually. However, this approach won't work, and there is one very simple but very important reason why:

Amplify subscriptions only work on the GraphQL layer. Since subscriptions are actually a GraphQL feature and not Amplify, changes in the data that come from any other vector different than GraphQL will not trigger any subscription.

This is very important to keep in mind when looking for documentation: even though Amplify provides the frontend code to make GraphQL queries more friendly, the core backend service that handles query resolution and subscriptions is AppSync. And this doesn't only apply to GraphQL: Amplify is mostly a service orchestrator, but for implementation details or more specific issues, it's a good idea also to check the particular AWS service docs.

GraphQL mutations βœ…

The way subscriptions are triggered in AppSync is through mutations. When a mutation updates an entity, subscribers of that entity are notified of changes. You might guess that updating your data through a GraphQL mutation is a good way of testing subscriptions, and you are right! There is one caveat:

Mutations need to include the same selection set as the subscription in order to trigger it

Continuing with the Character example, let's say we add a new field called level, ending up with the following schema:

const schema = a
    .schema({
        Character: a
            .model({
                name: a.string().required(),
                healthPoints: a.integer().required(),
                level: a.integer().required()
            })
    })

If we keep the subscription we created as it is now, we'd start getting the new level field every time a Character is updated. That might not be a problem now, but if we start adding connections and complex fields to our Character, it will be.

To avoid that, we can declare which fields we want to get back from the entity we are subscribing to, like this:

client.models.Character
    .observeQuery(selectionSet: ['name', 'healthPoints'])
    .subscribe({ next: (updatedList) => setCharacters([...updatedList.items]) })

This is correctly configured. However, something to keep in mind when writing manual mutations to trigger subscriptions is that for a mutation to trigger a subscription, it needs to have selection set parity with the subscription. That means, if our subscription is asking for name and healthPoints, the mutation should also ask for name and healthPoints as a response. For example:

mutation WrongUpdateCharacter {
    updateCharacter(input: { name: "Mario Santos", healthPoints: 15 }) {
        name
        level
        // ❌ This mutation wouldn't trigger our subscription, we're missing
        // healthPoints
    }
}

mutation RightUpdateCharacter {
    updateCharacter(input: { name: "Mario Santos", healthPoints: 15 }) {
        name
        level
        healthPoints // βœ…
    }
}

Frontend components βœ…

I'll admit it: subscriptions haven't behaved reliably when tested from a GraphQL client such as Postman, Insomnia, or even the Amplify API playground itself. That's why, when everything fails or when I'm giving a demo, I'll simply add a button somewhere in the frontend that, using the Amplify frontend client, triggers the update I want to see reflected in real time.

The code for that button would look something like this:

const handleClick = () => client.models.Character.update({
    name: 'Mario Santos',
    healthPoints: 15
})

Achieving the same results as the GraphQL query that we wrote in the previous section. It might not be the quickest solution (or the one that I'd want to use most of the time), but it has been the most reliable.

As an alternative if we don't want to modify the frontend (say, if we're doing a demo in a deployed environment), a very annoying but reliable solution could be to add a Lambda function that, when invoked, will trigger an update, in a similar fashion to how we configured the button above. Then, we can go to the AWS Lambda console and manually invoke it when it’s showtime.


AWS Amplify has proven to be a very useful tool for building production-ready apps with minimal infrastructure efforts. Some features it offers are in early stages, where documentation doesn't cover everything, especially regarding subscriptions. That's why I believe it's essential for devs like us to share what we learn and build a community of practitioners to fill in the gaps. Do you have any other learnings and tips you discovered while using Amplify?

0
Subscribe to my newsletter

Read articles from Mauro Bosetti directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mauro Bosetti
Mauro Bosetti

Proud graduate from National University of Quilmes, a public and free university in Argentina. Passionately argues about Object Oriented Programming, TDD and pair programming, plus whatever obscure technology I’ve been diving in lately.