AWS CDK: AppSync Events API with Cognito for WebSockets and React

January 19th, 2025 944 Words

The Amazon AppSync Events API was recently announced and is a new feature to use a WebSocket API for real-time communication. Based on the Amazon Cognito User Pool with Managed Login and guide for GraphQL Data API in Amazon AppSync, this guide shows you how to add real-time communication to your React application using WebSockets and the Amazon AppSync Events API with Amazon Cognito.

Amazon AppSync - and GraphQL in general - do support real-time communication; it’s called GraphQL Subscriptions are usually tricky to configure. GraphQL is great for fetching data; and Amazon AppSync is great for building a serverless GraphQL API. There is no need for a GraphQL subscriptions layers, there is just the need for a real-time communication layer!

Here comes Amazon AppSync Events API with WebSockets and some new APIs to publish messages to the clients and for clients, to subscribe to channels.

Amazon AppSync Events

Within Amazon AppSync Events, namespace and channel are the two main concepts. A namespace is a container for channels. A channel is a container for messages. Clients can subscribe to a channel (public/* or private/user-id e.g.) and receive messages. You can publish messages to a channel using a HTTP request and clients subscribe to a channel using a WebSocket connection.

For authentication, Amazon AppSync Events supports the same authentication methods as the GraphQL Data API: Amazon Cognito, AWS IAM, API Key, OpenID Connect, and AWS Lambda.

Amazon AppSync Events API with AWS CDK

In addition to the maybe existing GraphQL Data API, Amazon AppSync now supports a new kind of API: the GraphQL Events API. Therefore, you need to create a new AppSync API in your CDK CloudFormation Stack:

const events = new aws_appsync.CfnApi(stack, "Events", {
  name: `serverless-events`,
  eventConfig: {
    authProviders: [
      {
        authType: AuthorizationType.USER_POOL,
        cognitoConfig: {
          awsRegion: Stack.of(stack).region,
          userPoolId: users.userPoolId,
        },
      },
      { authType: AuthorizationType.IAM },
    ],
    connectionAuthModes: [{ authType: AuthorizationType.USER_POOL }],
    defaultPublishAuthModes: [{ authType: AuthorizationType.USER_POOL }],
    defaultSubscribeAuthModes: [{ authType: AuthorizationType.USER_POOL }],
  },
});

This will create a new AppSync Events API and enable two authentication methods:

  • CognitoUser Pool Authentication, and
  • AWS IAM Authentication.

Clients using the WebSocket connection will use Cognito User Pools and other services will use IAM authentication to publish messages to the AppSync Events API. Individual authentication modes can be configured per namespace and the default authentication is set to Cognito User Pools here.

Configure AppSync Namespaces

This creates two new namespace configurations: public and private. For the public namespace, clients can connect to all channels. For the private namespace, clients can only connect to a private channel with their user ID. AWS AppSync Events supports custom handlers for onSubscribe and onPublish for every namespace.

new CfnChannelNamespace(stack, "ChannelNamespacePublic", {
  name: `public`,
  apiId: events.attrApiId,
  publishAuthModes: [{ authType: AuthorizationType.IAM }],
  subscribeAuthModes: [{ authType: AuthorizationType.USER_POOL }],
});

For the public channel, restricting the publishAuthModes to IAM authentication is sufficient; for the private channel, we need to implement a custom handler for the onSubscribe event:

new CfnChannelNamespace(stack, "ChannelNamespacePrivate", {
  name: `private`,
  apiId: events.attrApiId,
  codeHandlers: `import { util } from '@aws-appsync/utils'

export function onSubscribe(ctx) {
  if (ctx.info.channel.path !== '/private/' + ctx.identity.sub) {
    console.log(ctx.identity.sub + 'tried connecting to wrong channel: ' + ctx.channel)

    util.unauthorized()
  }
}`,
  publishAuthModes: [{ authType: AuthorizationType.IAM }],
  subscribeAuthModes: [{ authType: AuthorizationType.USER_POOL }],
});

Run cdk deploy to create the AppSync Events API and the two namespaces.

AppSync Events API and GraphQL Data API in the AWS Management Console

You can also see the two namespaces in the AWS Management Console:

AppSync Events API and GraphQL Data API in the AWS Management Console

Next, we need to configure the React application to establish a WebSocket connection to the AppSync Events API.

Configure React & WebSocket for AppSync Events

Based on the Cognito User Pool with Managed Login and guide for GraphQL Data API in AWS AppSync, there is a working React application that uses the Apollo Client to fetch data from the AppSync GraphQL Data API; and most importantly, this shows how to use useAuth() hook to get the Cognito Access Token.

For the WebSocket connection, there exists an official AppSync Events API Client; but a normal WebSocket connection is also compatible with the AppSync. So let’s use the raw WebSocket connection for this:

export default function App() {
  const auth = useAuth();

  const authData = JSON.stringify({
    host: `your-id.appsync-api.eu-central-1.amazonaws.com`,
    Authorization: auth.user!.access_token,
  });

  socket = new WebSocket(
    `wss://your-api.appsync-realtime-api.eu-central-1.amazonaws.com/event/realtime`,
    [
      "aws-appsync-event-ws",
      `header-${btoa(authData)
        .replace(/\+/g, "-")
        .replace(/\//g, "_")
        .replace(/=+$/, "")}`,
    ]
  );

  socket.onopen = () => {
    socket!.send(JSON.stringify({ type: "connection_init" }));
  };
}

The WebSocket endpoint is available as the realtime endpoint of your AppSync Events API; the connection is authenticated using the Cognito Access Token and passed to the WebSocket connection as additional headers.

As soon as the connection is established, you can subscribe to a channel and receive messages. Wait for the connection_ack message and then you can send subscribe message types to the WebSocket connection.

const subscribe = (channel: string) => {
  socket.send(
    JSON.stringify({
      type: "subscribe",
      id: crypto.randomUUID(),
      channel,
      authorization: {
        host: `your-id.appsync-api.eu-central-1.amazonaws.com`,
        Authorization: auth.user!.access_token,
      },
    })
  );
};

socket.onmessage = (event) => {
  const data = JSON.parse(event.data);

  console.log(data);

  if (data.type === "connection_ack") {
    subscribe("public/*");
    subscribe(`private/${auth.user!.profile.sub}`);
  }
};

This is the basic connection and subscription handling for the AppSync Events API using basic WebSocket connection without the Amplify library. Most importantly, this uses the same Cognito session as the AppSync GraphQL Data API; both using the same react-oidc-context library with additional complexity or overhead.

Publish Messages with Events Rest API

As the authentication method for the AppSync Events API is Cognito User Pools for all subscriptions and AWS_IAM for publishing messages, AWS AppSync can publish messages using the AWS Management Console.

Publish message to AppSync Events API

Provide at least one message and select the public or private namespace; you should be able to publish messages to the AppSync Events API and retrieve them using the WebSocket connection in your React application. Awesome! 🎉