Connect to AWS AppSync Events
This guide walks through how you can connect to AWS AppSync Events using the Amplify library.
AWS AppSync Events lets you create secure and performant serverless WebSocket APIs that can broadcast real-time event data to millions of subscribers, without you having to manage connections or resource scaling. With this feature, you can build multi-user features such as a collaborative document editors, chat apps, and live polling systems.
Learn more about AWS AppSync Events by visiting the Developer Guide.
Install the AWS AppSync Events library
-
Add
AWS AppSync Events Library for Swift
into your project using Swift Package Manager. -
Enter its Github URL (https://github.com/aws-amplify/aws-appsync-events-swift), select
Up to Next Major Version
and clickAdd Package
. -
Select the following product and add it to your target:
AWSAppSyncEvents
-
Add
AWS AppSync Events Library for Swift
into your project using Swift Package Manager.- Enter its Github URL (https://github.com/aws-amplify/aws-appsync-events-swift), select
Up to Next Major Version
and clickAdd Package
. - Select the following product and add it to your target:
AWSAppSyncEvents
- Enter its Github URL (https://github.com/aws-amplify/aws-appsync-events-swift), select
-
Add
Amplify Library for Swift
into your project using Swift Package Manager.- Enter its Github URL (https://github.com/aws-amplify/amplify-swift), select
Up to Next Major Version
and clickAdd Package
. - Select the following product and add it to your target:
Amplify
AWSCognitoAuthPlugin
- Enter its Github URL (https://github.com/aws-amplify/amplify-swift), select
Providing AppSync Authorizers
The AWS AppSync Events library imports a number of Authorizer classes to match the various authorization strategies that may be used for your Events API. You should choose the appropriate Authorizer type for your authorization strategy.
- API KEY authorization, APIKeyAuthorizer
- AWS IAM authorization, IAMAuthorizer
- AMAZON COGNITO USER POOLS authorization, AuthTokenAuthorizer
You can create as many Events
clients as necessary if you require multiple authorization types.
API KEY
An APIKeyAuthorizer
can be used with a hardcoded API key or by fetching the key from some source.
// Use a hard-coded API keylet apiKeyAuthorizer = APIKeyAuthorizer(apiKey: "apiKey")// or// Fetch the API key from some source. This function may be called many times,// so it should implement appropriate caching internally.let authorizer = APIKeyAuthorizer(fetchAPIKey: { // fetch your api key })
AMAZON COGNITO USER POOLS
When working directly with AppSync, you must implement the token fetching yourself.
// Use your own token fetching. This function may be called many times,// so it should implement appropriate caching internally.let authorizer = AuthTokenAuthorizer(fetchLatestAuthToken: { // fetch your auth token})
AWS IAM
When working directly with AppSync, you must implement the request signing yourself.
// Provide an implementation of the signing function. This function should implement the // AWS Sig-v4 signing logic and return the authorization headers containing the token and signature.let authorizer = IAMAuthorizer(signRequest: { // implement your `URLRequest` signing logic})
The AWS AppSync Events library imports a number of Authorizer classes to match the various authorization strategies that may be used for your Events API. You should choose the appropriate Authorizer type for your authorization strategy.
- API KEY authorization, APIKeyAuthorizer
- AWS IAM authorization, IAMAuthorizer
- AMAZON COGNITO USER POOLS authorization, AuthTokenAuthorizer
You can create as many Events
clients as necessary if you require multiple authorization types.
API KEY
An APIKeyAuthorizer
can be used with a hardcoded API key or by fetching the key from some source.
// Use a hard-coded API keylet apiKeyAuthorizer = APIKeyAuthorizer(apiKey: "apiKey")// or// Fetch the API key from some source. This function may be called many times,// so it should implement appropriate caching internally.let authorizer = APIKeyAuthorizer(fetchAPIKey: { // fetch your api key })
AMAZON COGNITO USER POOLS
If you are using Amplify Auth, you can create a method that retrieves the Cognito access token.
import Amplify
func getUserPoolAccessToken() async throws -> String { let authSession = try await Amplify.Auth.fetchAuthSession() if let result = (authSession as? AuthCognitoTokensProvider)?.getCognitoTokens() { switch result { case .success(let tokens): return tokens.accessToken case .failure(let error): throw error } } throw AuthError.unknown("Did not receive a valid response from fetchAuthSession for get token.")}
Then create the AuthTokenAuthorizer
with this method.
let authorizer = AuthTokenAuthorizer(fetchLatestAuthToken: getUserPoolAccessToken)
AWS IAM
If you are using Amplify Auth, you can use the following class to implement SigV4 signing logic:
import Foundationimport Amplify import AWSPluginsCore import AwsCommonRuntimeKit import AWSSDKHTTPAuth import Smithy import SmithyHTTPAPIimport SmithyHTTPAuthimport SmithyHTTPAuthAPIimport SmithyIdentity
class AppSyncEventsSigner { public static func createAppSyncSigner(region: String) -> ((URLRequest) async throws -> URLRequest) { return { request in try await signAppSyncRequest(request, region: region) } } private static var signer = { return AWSSigV4Signer() }() static func signAppSyncRequest(_ urlRequest: URLRequest, region: Swift.String, signingName: Swift.String = "appsync", date: Date = Date()) async throws -> URLRequest { CommonRuntimeKit.initialize() // Convert URLRequest to SDK's HTTPRequest guard let requestBuilder = try createAppSyncSdkHttpRequestBuilder( urlRequest: urlRequest) else { return urlRequest } // Retrieve the credentials from credentials provider let credentials: AWSCredentialIdentity let authSession = try await Amplify.Auth.fetchAuthSession() if let awsCredentialsProvider = authSession as? AuthAWSCredentialsProvider { let awsCredentials = try awsCredentialsProvider.getAWSCredentials().get() credentials = try awsCredentials.toAWSSDKCredentials() } else { let error = AuthError.unknown("Auth session does not include AWS credentials information") throw error } // Prepare signing let flags = SigningFlags(useDoubleURIEncode: true, shouldNormalizeURIPath: true, omitSessionToken: false) let signedBodyHeader: AWSSignedBodyHeader = .none let signedBodyValue: AWSSignedBodyValue = .empty let signingConfig = AWSSigningConfig(credentials: credentials, signedBodyHeader: signedBodyHeader, signedBodyValue: signedBodyValue, flags: flags, date: date, service: signingName, region: region, signatureType: .requestHeaders, signingAlgorithm: .sigv4) // Sign request guard let httpRequest = await signer.sigV4SignedRequest( requestBuilder: requestBuilder, signingConfig: signingConfig ) else { return urlRequest } // Update original request with new headers return setHeaders(from: httpRequest, to: urlRequest) } static func setHeaders(from sdkRequest: SmithyHTTPAPI.HTTPRequest, to urlRequest: URLRequest) -> URLRequest { var urlRequest = urlRequest for header in sdkRequest.headers.headers { urlRequest.setValue(header.value.joined(separator: ","), forHTTPHeaderField: header.name) } return urlRequest } static func createAppSyncSdkHttpRequestBuilder(urlRequest: URLRequest) throws -> HTTPRequestBuilder? { guard let url = urlRequest.url, let host = url.host else { return nil } let headers = urlRequest.allHTTPHeaderFields ?? [:] let httpMethod = (urlRequest.httpMethod?.uppercased()) .flatMap(HTTPMethodType.init(rawValue:)) ?? .get let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems? .map { URIQueryItem(name: $0.name, value: $0.value)} ?? [] let requestBuilder = HTTPRequestBuilder() .withHost(host) .withPath(url.path) .withQueryItems(queryItems) .withMethod(httpMethod) .withPort(443) .withProtocol(.https) .withHeaders(.init(headers)) .withBody(.data(urlRequest.httpBody)) return requestBuilder }}
extension AWSPluginsCore.AWSCredentials { func toAWSSDKCredentials() throws -> AWSCredentialIdentity { if let tempCredentials = self as? AWSTemporaryCredentials { return AWSCredentialIdentity( accessKey: tempCredentials.accessKeyId, secret: tempCredentials.secretAccessKey, expiration: tempCredentials.expiration, sessionToken: tempCredentials.sessionToken ) } else { return AWSCredentialIdentity( accessKey: accessKeyId, secret: secretAccessKey, expiration: nil ) } }}
Then, create an IAMAuthorizer
with this helper class.
let authorizer = IAMAuthorizer( signRequest: AppSyncEventsSigner.createAppSyncSigner(region: "region"))
Connect to an Event API without an existing Amplify backend
Before you begin, you will need:
- An Event API created via the AWS Console
- Take note of: HTTP endpoint, region, API Key
Thats it! Skip to Client Library Usage Guide.
Add an Event API to an existing Amplify backend
This guide walks through how you can add an Event API to an existing Amplify backend. We'll be using Cognito User Pools for authenticating with Event API from our frontend application. Any signed in user will be able to subscribe to the Event API and publish events.
Before you begin, you will need:
- An existing Amplify backend (see Quickstart)
- Latest versions of
@aws-amplify/backend
and@aws-amplify/backend-cli
(npm add @aws-amplify/backend@latest @aws-amplify/backend-cli@latest
)
Update Backend Definition
First, we'll add a new Event API to our backend definition.
import { defineBackend } from '@aws-amplify/backend';import { auth } from './auth/resource';// import CDK resources:import { CfnApi, CfnChannelNamespace, AuthorizationType,} 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 customResources = backend.createStack('custom-resources');
// add a new Event API to the stack:const cfnEventAPI = new CfnApi(customResources, 'CfnEventAPI', { name: 'my-event-api', eventConfig: { authProviders: [ { authType: AuthorizationType.USER_POOL, cognitoConfig: { awsRegion: customResources.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( customResources, '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(customResources, 'AppSyncEventPolicy', { statements: [ new PolicyStatement({ actions: [ 'appsync:EventConnect', 'appsync:EventSubscribe', 'appsync:EventPublish', ], resources: [`${cfnEventAPI.attrApiArn}/*`, `${cfnEventAPI.attrApiArn}`], }), ], }));
Deploy Backend
To test your changes, deploy your Amplify Sandbox.
npx ampx sandbox
Client Library Usage Guide
Create the Events class
You can find your endpoint in the AWS AppSync Events console. It should start with https
and end with /event
.
let eventsEndpoint = "https://abcdefghijklmnopqrstuvwxyz.appsync-api.us-east-1.amazonaws.com/event"let events = Events(endpointURL: eventsEndpoint)
Using the REST Client
An EventsRestClient
can be created to publish event(s) over REST. It accepts a publish authorizer that will be used by default for any publish calls within the client.
Creating the REST Client
let events = Events(endpointURL: eventsEndpoint)let restClient = events.createRestClient( publishAuthorizer: APIKeyAuthorizer(apiKey: "apiKey"))
Additionally, you can pass custom options to the Rest Client. Current capabilities include passing a custom URLSessionConfiguration
object, a prepend URLRequestInterceptor
and enabling client library logs.
See Collecting Client Library Logs and RestOptions
class for more details.
let restClient = events.createRestClient( publishAuthorizer: apiKeyAuthorizer, options: .init( urlSessionConfiguration: urlSessionConfiguration, // your instance of `URLSessionConfiguration` logger: AppSyncEventsLogger(), // your implementation of `EventsLogger` interceptor: AppSyncEventsURLRequestInterceptor() // your implementation of `URLRequestInterceptor` ))
Publish a single event
let defaultChannel = "default/channel"let event = JSONValue(stringLiteral: "123")do { let result = try await restClient.publish( channelName: defaultChannel, event: event ) print("Publish success with result:\n status: \(result.status) \n, successful events: \(result.successfulEvents) \n, failed events: \(result.failedEvents)")} catch { print("Publish failure with error: \(error)")}
Publish multiple events
You can publish up to 5 events at a time.
let defaultChannel = "default/channel"let eventsList = [ JSONValue(stringLiteral: "123"), JSONValue(booleanLiteral: true), JSONValue(floatLiteral: 1.25), JSONValue(integerLiteral: 37), JSONValue(dictionaryLiteral: ("key", "value"))]
do { let result = try await restClient.publish( channelName: defaultChannel, events: eventsList ) print("Publish success with result:\n status: \(result.status) \n, successful events: \(result.successfulEvents) \n, failed events: \(result.failedEvents)")} catch { print("Publish failure with error: \(error)")}
Publish with a different authorizer
let defaultChannel = "default/channel"let event = JSONValue(stringLiteral: "123")let apiKeyAuthorizer = APIKeyAuthorizer(apiKey: "apiKey")do { let result = try await restClient.publish( channelName: defaultChannel, event: event, authorizer: apiKeyAuthorizer ) print("Publish success with result:\n status: \(result.status) \n, successful events: \(result.successfulEvents) \n, failed events: \(result.failedEvents)")} catch { print("Publish failure with error: \(error)")}
Using the WebSocket Client
An EventsWebSocketClient
can be created to publish and subscribe to channels. The WebSocket connection is managed by the library and connects on the first subscribe or publish operation. Once connected, the WebSocket will remain open. You should explicitly disconnect the client when you no longer need to subscribe or publish to channels.
Creating the WebSocket Client
let apiKeyAuthorizer = APIKeyAuthorizer(apiKey: "apiKey")let webSocketClient = events.createWebSocketClient( connectAuthorizer: apiKeyAuthorizer, publishAuthorizer: apiKeyAuthorizer, subscribeAuthorizer: apiKeyAuthorizer)
Additionally, you can pass custom options to the WebSocket Client. Current capabilities include passing a custom URLSessionConfiguration
object, a prepend URLRequestInterceptor
and enabling client library logs.
See Collecting Client Library Logs and WebSocketOptions
class for more details.
let apiKeyAuthorizer = APIKeyAuthorizer(apiKey: "apiKey")let webSocketClient = events.createWebSocketClient( connectAuthorizer: apiKeyAuthorizer, publishAuthorizer: apiKeyAuthorizer, subscribeAuthorizer: apiKeyAuthorizer, options: .init( urlSessionConfiguration: urlSessionConfiguration, // your instance of `URLSessionConfiguration` logger: AppSyncEventsLogger(), // your implementation of `EventsLogger` interceptor: AppSyncEventsURLRequestInterceptor() // your implementation of `URLRequestInterceptor` ) )
Publish a Single Event
let defaultChannel = "default/channel"let event = JSONValue(stringLiteral: "123")do { let result = try await websocketClient.publish( channelName: defaultChannel, event: event ) print("Publish success with result:\n status: \(result.status) \n, successful events: \(result.successfulEvents) \n, failed events: \(result.failedEvents)")} catch { print("Publish failure with error: \(error)")}
Publish multiple Events
You can publish up to 5 events at a time.
let defaultChannel = "default/channel"let eventsList = [ JSONValue(stringLiteral: "123"), JSONValue(booleanLiteral: true), JSONValue(floatLiteral: 1.25), JSONValue(integerLiteral: 37), JSONValue(dictionaryLiteral: ("key", "value"))]
do { let result = try await websocketClient.publish( channelName: defaultChannel, events: eventsList ) print("Publish success with result:\n status: \(result.status) \n, successful events: \(result.successfulEvents) \n, failed events: \(result.failedEvents)")} catch { print("Publish failure with error: \(error)")}
Publish with a different authorizer
let defaultChannel = "default/channel"let event = JSONValue(stringLiteral: "123")let apiKeyAuthorizer = APIKeyAuthorizer(apiKey: "apiKey")do { let result = try await websocketClient.publish( channelName: defaultChannel, event: event, authorizer: apiKeyAuthorizer ) print("Publish success with result:\n status: \(result.status) \n, successful events: \(result.successfulEvents) \n, failed events: \(result.failedEvents)")} catch { print("Publish failure with error: \(error)")}
Subscribing to a channel
When subscribing to a channel, you can subscribe to a specific namespace/channel (e.g. default/channel
), or you can specify a wildcard (*
) at the end of a channel path to receive events published to all channels that match (e.g. default/*
).
let defaultChannel = "default/channel"let subscription = try websocketClient.subscribe(channelName: defaultChannel)let task = Task { for try await message in subscription { print("Subscription received message: \(message))" }}
To unsubscribe from the channel, you can cancel the enclosing task for the AsyncThrowingStream
.
task.cancel()
Subscribing to a channel with a different authorizer
let defaultChannel = "default/channel"let apiKeyAuthorizer = APIKeyAuthorizer(apiKey: "apiKey")let subscription = try websocketClient.subscribe( channelName: defaultChannel, authorizer: apiKeyAuthorizer)let task = Task { for try await message in subscription { print("Subscription received message: \(message))" }}
Disconnecting the WebSocket
When you are done using the WebSocket and do not intend to call publish/subscribe on the client, you should disconnect the WebSocket. This will unsubscribe all channels.
// set flushEvents to true if you want to wait for any pending publish operations to post to the WebSocket// set flushEvents to false to immediately disconnect, discarding any pending posts to the WebSockettry await webSocketClient.disconnect(flushEvents: true) // or false to immediately disconnect
Collecting Client Library Logs
In the Rest Client and WebSocket Client examples, we demonstrated logging to a custom logger. Here is an example of a custom logger that writes logs to Xcode console. You are free to implement your own EventsLogger
type.
import osimport Foundationimport AWSAppSyncEvents
public final class AppSyncEventsLogger: EventsLogger { static let lock: NSLocking = NSLock()
static var _logLevel = LogLevel.error public init() { } public var logLevel: LogLevel { get { AppSyncEventsLogger.lock.lock() defer { AppSyncEventsLogger.lock.unlock() }
return AppSyncEventsLogger._logLevel } set { AppSyncEventsLogger.lock.lock() defer { AppSyncEventsLogger.lock.unlock() }
AppSyncEventsLogger._logLevel = newValue } }
public func error(_ log: @autoclosure () -> String) { os_log("%@", type: .error, log()) }
public func error(_ error: @autoclosure () -> Error) { os_log("%@", type: .error, error().localizedDescription) }
public func warn(_ log: @autoclosure () -> String) { guard logLevel.rawValue >= LogLevel.warn.rawValue else { return }
os_log("%@", type: .info, log()) }
public func info(_ log: @autoclosure () -> String) { guard logLevel.rawValue >= LogLevel.info.rawValue else { return }
os_log("%@", type: .info, log()) }
public func debug(_ log: @autoclosure () -> String) { guard logLevel.rawValue >= LogLevel.debug.rawValue else { return }
os_log("%@", type: .debug, log()) }
public func verbose(_ log: @autoclosure () -> String) { guard logLevel.rawValue >= LogLevel.verbose.rawValue else { return }
os_log("%@", type: .debug, log()) }}