In this article, I will explore AWS AppSync Events, a feature that enables easy adoption and maintenance of WebSocket APIs and most importantly using Serverless architecture. As you will see, with the help of Amplify Gen 2, we will be able to quickly deploy a serverless chat application to broadcast real-time event data to multiple subscribers.
Before we dive into, let’s review some concepts to refresh our memory:
- Amplify Gen 2: Amplify Gen 2 transitions to a Typescript developer experience, allowing developers to build full-stack cloud applications using just Typescript, while the cloud infrastructure is automatically deployed based on the declared application code. With Gen 2, your back-end features are built with AWS Cloud Development Kit (AWS CDK). I recently wrote an article here that will serve as the foundation for our Chat example.
- AppSync: AWS AppSync is a fully managed service that enables you to build GraphQL APIs easily. It allows applications to securely access, manipulate, and combine data from multiple sources like DynamoDB, RDS, Lambda, and Elasticsearch (OpenSearch Service). It also supports WebSocket for real-time subscriptions and offline sync for mobile/web apps.
- WebSocket APIs: WebSocket APIs provide real-time, bi-directional communication between clients and servers. Unlike REST APIs, where clients must repeatedly poll for updates, WebSocket APIs maintain a persistent connection, reducing latency and improving efficiency.
Why AppSync Events instead of common WebSocket servers?
AppSync Events lets you create serverless WebSocket APIs, without you having to manage connections or resource scaling. You have automatic management on WebSocket connection and built-in support for broadcasting events to large numbers of subscribers. Find out more in the official documentation here.
A serverless live chat
To start, we need to create the project, for that, you can use Amplify Gen 2 quickstart. Once you have everything up and running, the first resource we need to configure is an AppSync API of type Event. The following code below is an example of how to attach on the backend:
import { defineBackend } from '@aws-amplify/backend';
import { auth } from './auth/resource';
import { AuthorizationType, CfnApi, CfnChannelNamespace } from 'aws-cdk-lib/aws-appsync';
import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam';
const backend = defineBackend({
auth
});
// create a new stack for our Event API resources:
const customStack = backend.createStack('custom-stack');
// add a new Event API to the stack:
const cfnEventAPI = new CfnApi(customStack, 'CfnEventAPI', {
name: 'events',
eventConfig: {
authProviders: [
{
authType: AuthorizationType.USER_POOL,
cognitoConfig: {
awsRegion: customStack.region,
// configure Event API to use the Cognito User Pool provisioned by Amplify:
userPoolId: backend.auth.resources.userPool.userPoolId
}
}
],
// configure the User Pool as the auth provider for Connect, Publish, and Subscribe operations:
connectionAuthModes: [{ authType: AuthorizationType.USER_POOL }],
defaultPublishAuthModes: [{ authType: AuthorizationType.USER_POOL }],
defaultSubscribeAuthModes: [{ authType: AuthorizationType.USER_POOL }]
}
});
// create a default namespace for our Event API:
const namespace = new CfnChannelNamespace(customStack, 'CfnEventAPINamespace', {
apiId: cfnEventAPI.attrApiId,
name: 'default'
});
// attach a policy to the authenticated user role in our User Pool to grant access to the Event API:
backend.auth.resources.authenticatedUserIamRole.attachInlinePolicy(
new Policy(customStack, 'AppSyncEventPolicy', {
statements: [
new PolicyStatement({
actions: ['appsync:EventConnect', 'appsync:EventSubscribe', 'appsync:EventPublish'],
resources: [`${cfnEventAPI.attrApiArn}/*`, `${cfnEventAPI.attrApiArn}`]
})
]
})
);
backend.addOutput({
custom: {
events: {
url: `https://${cfnEventAPI.getAtt('Dns.Http').toString()}/event`,
aws_region: customStack.region,
default_authorization_type: AuthorizationType.USER_POOL
}
}
});
On the code above, we have four main resources:
- An AppSync API of type Event that is authenticated via Cognito User Pool. You can find here how to create/integrate Cognito to your application.
- A default namespace for the Event API.
- IAM Policy that permits the AppSync API to connect, subscribe and publish events to the API.
- Custom API declaration for amplify backend authenticated via Cognito User Pool.
Once backend configuration is set, is time to deploy into sandbox config so we can use its resources on front-end. For that, run the following command:
npx ampx sandbox
If you would like to know more about how sandbox environments work, please check its official documentation here.
Designing the UI
Now that the backend is developed and deployed via sandbox , we can integrate to the UI components. And, to exemplify the connection between two channels, I designed the UI to have two chat component, simulating two distinct users interacting with each other.
Chat component
import { ChangeEvent, SyntheticEvent, useEffect, useRef, useState } from 'react';
import {
ButtonDiv,
ChatMessageDiv,
FeedArea,
FeedTextArea,
InputAreaDiv,
LeftMessageDiv,
RightMessageDiv,
StyledButton,
StyledInput,
TimeStampDiv,
UserMessageDiv,
Wrapper
} from './styled';
import { v4 as uuidv4 } from 'uuid';
import { events } from 'aws-amplify/data';
import type { EventsChannel } from 'aws-amplify/data';
import { getTimeFormat } from './utils';
interface ChatProps {
username: string;
}
export interface Message {
id: string;
username: string;
message: string;
timestamp: string;
}
const Chat: React.FC = ({ username }) => {
const [chatUserId] = useState(uuidv4());
const inputRef = useRef(null);
const [inputValue, setInputValue] = useState('');
const [chatAreaState, setChatAreaState] = useState>([]);
const serverRowComponent = (userMessage: Message): JSX.Element => {
const newUuid = uuidv4();
return (
<>
{' '}
{userMessage.username}:
{userMessage.message}
>
{userMessage.timestamp}
);
};
const userRowComponent = (userMessage: Message): JSX.Element => {
const newUuid = uuidv4();
return (
<>
{' '}
{userMessage.username}:
{userMessage.message}
>
{userMessage.timestamp}
);
};
function updateChatAreaState(element: JSX.Element) {
const currentAreaState = chatAreaState;
currentAreaState.push(element);
setChatAreaState(currentAreaState);
}
function handleChange(event: ChangeEvent) {
setInputValue(event.target.value);
}
async function handleSubmit(event: SyntheticEvent) {
if (inputValue !== '') {
const message: Message = {
id: chatUserId,
username: username,
message: inputValue,
timestamp: getTimeFormat()
};
await events.post('default/channel', { message: { ...message } });
updateChatAreaState(userRowComponent(message));
setInputValue('');
event.preventDefault();
}
}
async function connectAndSubscribe() {
let channel: EventsChannel;
channel = await events.connect('default/channel');
channel.subscribe({
next: (data: any) => {
const incoming = data?.event?.message;
if (incoming.id && incoming.id !== chatUserId) {
const displayMessage: Message = {
id: incoming.id,
username: incoming.username,
message: incoming.message,
timestamp: getTimeFormat()
};
updateChatAreaState(serverRowComponent(displayMessage));
}
},
error: (err: any) => {
console.error('Subscription error:', {
message: err.message,
stack: err.stack,
details: err
});
}
});
return () => channel && channel.close();
}
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
connectAndSubscribe();
}, []);
return (
{chatAreaState}
handleSubmit(event)}>Submit
);
};
export default Chat;
Landing component
import Chat from '../../custom/chat';
import { Container, SplitDiv, UserDiv } from './styled';
const Landing: React.FC = () => {
const westUser = 'John Doe';
const eastUser = 'Jane Smith';
return (
<>Workspace: {westUser}>
<>Workspace: {eastUser}>
);
};
export default Landing;
With the front-end finished let’s see the result:
And, as simple as that, we have a Chat application using Serverless WebSocket resources, easy to deploy, auto scalable and the most important cost effective.
Monitoring logs & metrics
Enable logs
You can enable the logging of CloudWatch metrics in AWS Amplify Gen 2, and you can utilise the Amplify Logger to send logs from your application to Amazon CloudWatch.
In your Amplify backend configuration, you can enable logging and specify settings, such as log level and retention period. This setup ensures that your application’s interactions with AWS services are logged appropriately. Here’s how to set it up in your backend application:
export const data = defineData({
// ... other configurations ...
logging: {
excludeVerboseContent: false,
fieldLogLevel: 'all',
retention: '1 month',
},
});
When excludeVerboseContent is set to false, logs will include full queries and user parameters, which can contain sensitive data. So, be careful with your environment configuration and ensure that access to these logs is restricted to non-prod and necessary personnel.
Subscribe to real-time metrics
AWS AppSync provides real-time metrics that focus on WebSocket connections and subscription activities. They include metrics for connection requests, subscription registrations, message publishing, active connections and many other attributes.
You can access CloudWatch metrics in the AWS Management Console, but if you also want to keep track of the WebSocket connections, you can create custom AWS CloudWatch alarms and subscribe to these events. You can easily configure the subscription on AWS Management Console or add into your application via AWS CDK, which would be integrated with Amplify Gen 2.
Here is an AWS guide on how to create AWS CloudWatch alarms using CDK. The list of all metrics for real-time events can be found here.
AWS AppSync Events pricing
The pricing for AppSync Events is based by the number of inbound and outbound messages, total event request handlers, client connection requests and subscriptions, and the total minutes of clients connected. By doing a simple comparison in price, and according to the AWS AppSync pricing page, an application with real-time events that publishes 10,000 messages on channel and has 1,000,000 clients connected will average $13.01 per month in its costs. You can check more in detail on the official page here.
Conclusion
AWS AppSync events unlock a new level of flexibility and efficiency, thanks to their serverless architecture. By eliminating the need for manual infrastructure management, developers can focus on building real-time, scalable applications without worrying about backend complexity.
Here’s why AppSync events are a game-changer:
- Scalability: Handles thousands of concurrent connections effortlessly without provisioning servers.
- Cost-Efficiency: Pay only for the resources used – no need for always-on instances.
- Seamless Integration: Works natively with AWS services like Lambda, DynamoDB, and EventBridge for event-driven workflows.
- Real-Time Updates: WebSocket enable instant data synchronisation across devices with minimal effort.
How Cevo can help in your Modernisation
If you are not sure that your platforms require modernisation, or need help to assess your workloads, review licensing, identify potential modernisation pathways, and build a business case for modernisation, reach out to us. With years of experience in the market, Cevo Australia will tailor a solution specifically for your requirements.
Alan Terriaga.
AWS Tech Specialist – Modernisation and Cloud Native