Front-End Web & Mobile

Building real-time apps with AWS AppSync Events’ WebSocket publishing

Real-time features have become essential in modern applications. Whether you’re building collaborative tools, live dashboards, or interactive games, users have come to expect instant and seamless updates as they interact with apps. AWS AppSync Events, a fully-managed service for serverless WebSocket APIs, has been helping developers add real-time capabilities to their applications, enabling them to build responsive and engaging experiences at scale.

Today, I’m excited to announce an enhancement to AWS AppSync Events: the ability to publish messages directly over WebSocket connections, in addition to publishing over the API’s HTTP endpoint. This update allows developers to use a single WebSocket connection for both publishing and receiving events, streamlining the development of real-time features and reducing implementation complexity. This is particularly beneficial for chatty applications that previously faced the overhead of establishing new HTTP connections for each message publication. Developers now have more flexibility: they can publish over HTTP endpoint, e.g.: for publishing from a backend, or publish over WebSocket which might be preferred for web and mobile application clients. In this post, I’ll go over the new enhancement, and show you how you can start integrating publishing over WebSocket in your apps.

Getting started

As I mentioned in my Announcing AWS AppSync Events post, getting started with AppSync Events is simple, and you can now publish over WebSocket or HTTP directly from the console. From the AppSync console, you can create an API and automatically get a default channel namespace along with an API key. on the Pub/Sub Editor, you can immediately try out your API. As shown in the image below, choose the “Publish” button, and in the dropdown, choose “WebSocket”. Your events are published over the WebSocket. You receive a publish_success message confirming the request.

AppSync Pub/Sub editor showing new "Publish" dropdown button with "HTTP", and "WebScoket" option

Publishing over WebSocket in the AppSync’s console Pub/Sub editor

Message format

AppSync now supports a new “publish” WebSocket operation to publish events. After connecting to the WebSocket, your client can start publishing events to channels in configured channel namespaces. Note that you do not need to subscribe to a channel before publishing to it. To publish events, you simply create a data message, specify an id that uniquely identifies the message, the channel you are sending the message to, the list of events your are sending (up to 5), and the authorization headers to allow the request. You can find more information about the authorization headers format in the documentation. Each event in the events array must be a valid JSON string. Here’s an example.

{
  "type": "publish",
  "id": "an-identifier-for-this-request",
  "channel": "/namespace/my/path",
  "events": [ "{ \"msg\": \"Hello World!\" }" ],
  "authorization": {
    "x-api-key": "da2-12345678901234567890123456-example",
   }
}
JSON

After publishing, you receive a “publish_success” response with details on each event sent, or a “publish_error” response if the operation was not successful.

Integrate with your application

In the previous post, I showed you how to implement a simple client that uses the browser’s Web API WebSocket. I’ll do this again to connect and publish on an Event API WebSocket endpoint. In this example, I’ll build a real-time demo chat application where clients can send and receive messages instantly over WebSocket. Messages are published to the /default/messages channel, and clients subscribe to /default/* to receive all messages in the default namespace.

A simple chat titled "Messages" show some received messages above an input form

A demo chat application

I’ll use the new AWS Cloud Development Kit (CDK) L2 constructs for AppSync Events to configure and deploy an AppSync Event API, with a single channel namespace named “default”, and a single API key. Learn more about getting started with AWS CDK. If needed, use the Node Package Manager to install the CDK CLI.

$ npm install -g aws-cdk
Shell

I start by initializing a new folder structure and create my CDK application.

$ mkdir -p events-app/cdk-events-publish
$ cd events-app/cdk-events-publish
$ cdk init app --language javascript
Shell

Next, I update the lib/cdk-events-publish-stack.js file with this code:

const { Stack, CfnOutput } = require('aws-cdk-lib');
const { EventApi, AppSyncAuthorizationType } = require('aws-cdk-lib/aws-appsync');

class CdkEventsPublishStack extends Stack {
  constructor(scope, id, props) {
    super(scope, id, props);
    const apiKeyProvider = { authorizationType: AppSyncAuthorizationType.API_KEY };

    // create an API called `my-event-api` that uses API Key authorization
    const api = new EventApi(this, 'api', {
      apiName: 'my-event-api',
      authorizationConfig: { authProviders: [apiKeyProvider] }
    });

    // add a channel namespace called `default`
    api.addChannelNamespace('default');

    // output configuration properties
    new CfnOutput(this, 'apiKey', { value: api.apiKeys['Default'].attrApiKey });
    new CfnOutput(this, 'httpDomain', { value: api.httpDns });
    new CfnOutput(this, 'realtimeDomain', { value: api.realtimeDns });
  }
}

module.exports = { CdkEventsPublishStack }
JavaScript

I then deploy the stack and save the output to a JSON file.

$ npm run cdk deploy -- -O output.json
Shell

I get output that looks like this:

Outputs:
CdkStack.apiKey = da2-12345678901234567890123456-example
CdkStack.httpDomain = a12345678901234567890123456.appsync-api.us-east-2.amazonaws.com
CdkStack.realtimeDomain = a12345678901234567890123456.appsync-realtime-api.us-east-2.amazonaws.com
Plain text

Next, I create the web app using vite (a frontend build tool), and vite’s vanilla javascript template. From the events-app folder, I run:

$ npm create vite@latest app -- --template vanilla
$ cd app
$ npm install
$ ln -s ../../cdk-events-publish/output.json src
Shell

This creates a link to the output.json file that I can refer to in the app. In the new app folder, I replace src/main.js with the code below. Note that the code uses the CDK output in output.json.

import './style.css'
import output from './output.json'

// use the output from the CDK stack deployment
const HTTP_DOMAIN = output.CdkEventsPublishStack.httpDomain
const REALTIME_DOMAIN = output.CdkEventsPublishStack.realtimeDomain
const API_KEY = output.CdkEventsPublishStack.apiKey 
     
const authorization = { 'x-api-key': API_KEY, host: HTTP_DOMAIN }

document.querySelector('#app').innerHTML = `
    <style>
      #top{width: calc(100vw - 8rem); max-width: 1280px; background: oklch(0.977 0.013 236.62); margin: 2rem 0; height: calc(100vh - 8rem); box-shadow: inset 0 0 20px rgba(0,0,0,0.1); position: relative; margin-inline: auto;}
      #container{display: flex;flex-direction: column;height: calc(100% - 6.5rem);}
      h2{color: oklch(0.3 0.013 236.62); margin-bottom: 1.5em; font-size: 1.8rem;}
      #messages{display: flex; flex-direction: column-reverse; gap: 1em; max-height: calc(100vh - 200px); flex: 1; min-height: 0; overflow-y: auto; text-align: left;}
      form{position: absolute; bottom: 2rem; left: 2rem; right: 2rem; padding: 1rem 0;}
      input{width: 90%; padding: 0.8em 1.2em; border: 1px solid oklch(0.8 0.013 236.62); border-radius: 25px; font-size: 1rem; outline: none; transition: all 0.2s ease; box-shadow: 0 2px 5px rgba(0,0,0,0.05);}
      .msg{padding: 0 1rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;}
    </style>
    <div id="top">
      <div id="container"><h2>Messages</h2><div id="messages"></div></div>
      <form id="form"> <input id="messageInput" name="message" type="text" autocomplete="off" /></form>
    </div>`

// construct the protocol header for the connection
function getAuthProtocol() {
  const header = btoa(JSON.stringify(authorization))
    .replace(/\+/g, '-') // Convert '+' to '-'
    .replace(/\//g, '_') // Convert '/' to '_'
    .replace(/=+$/, '') // Remove padding `=`
  return `header-${header}`
}

const socket = await new Promise((resolve, reject) => {
  const socket = new WebSocket(`wss://${REALTIME_DOMAIN}/event/realtime`, [
    'aws-appsync-event-ws',
    getAuthProtocol(),
  ])
  socket.onopen = () => resolve(socket)
  socket.onclose = (event) => reject(new Error(event.reason))
  socket.onmessage = (_evt) => {
    const data = JSON.parse(_evt.data)
    // if this is a `data` event, add the content to the list of messages
    if (data.type === 'data') {
      const event = JSON.parse(data.event)
      const div = document.createElement('div')
      div.className = 'msg'
      div.innerHTML = `${event.time} | ↓ ${new Date().toISOString().split('T')[1]} | ${event.message}`
      messages.prepend(div)
    }
  }
})

// subscribe to `/default/*`
socket.send(
  JSON.stringify({
    type: 'subscribe',
    id: crypto.randomUUID(),
    channel: '/default/*',
    authorization,
  }),
)

const form = document.querySelector('#form')
const messageInput = document.querySelector('#messageInput')
const messages = document.querySelector('#messages')

// when the form is submitted, send an event to `/default/messages`
form.addEventListener('submit', (e) => {
  e.preventDefault()
  const message = new FormData(e.currentTarget).get('message')
  messageInput.value = ''
  socket.send(
    JSON.stringify({
      type: 'publish',
      id: crypto.randomUUID(),
      channel: '/default/messages',
      events: [JSON.stringify({ message, time: new Date().toISOString().split('T')[1] })],
      authorization,
    }),
  )
})
JavaScript

Now in the app directory, I start the web server:

$ npm run dev
Shell

I can open the website at the provided address on multiple browsers to send and receive messages.

Cleaning up

When done with the example, I can remove the resources I deployed with CDK.

$ cd ../cdk-events-publish
$ npm run cdk destroy
Shell

Conclusion

In this post, I went over the new Publish over WebSocket feature for AWS AppSync Events, and showed how to get started with the feature. This enhancement offers several benefits:

  • Simplified implementation with a single connection for publishing and subscribing
  • Reduced connection overhead for chatty applications
  • Flexibility to choose between WebSocket and HTTP publishing based on your use case

Publishing over WebSocket is now available in all regions where AppSync is available. Clients can publish at a rate of 25 requests per second per client WebSocket connection. You can continue using your API’s HTTP endpoint to publish at higher rates (adjustable default of 10,000 events per second). Visit AppSync’s endpoints and quotas page for more details. We’re excited to see what you build with this new capability. Get started today by trying out the example in this post or by adding WebSocket publishing to your existing AppSync Events applications. To learn more about AppSync Events, visit the documentation.