Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions appsync-events-lambda-cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# AWS AppSync Events with Lambda

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AWS Lambda

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Updated all references to use the full service name 'AWS Lambda' consistently.


This pattern deploys an AppSync Events API for real-time WebSocket pub/sub with Lambda event processing.

@parikhudit parikhudit Jun 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Within the document the service is referenced as "AWS Lambda" inside the ASCII diagram, "Lambda" in the prose, and "lambda" in the metadata services block. Consistent first-reference usage of AWS Lambda and Amazon AppSync Events throughout improves readability. Same for AppSync Events and others.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Standardized to 'AWS Lambda' and 'AWS AppSync Events' throughout README and example-pattern.json.


Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/appsync-events-lambda-cdk

Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details.

## Requirements

* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
* [Node.js 22+](https://nodejs.org/en/download/) installed
* [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed

## Architecture

```
┌───────────┐ ┌─────────────────────┐ ┌──────────────┐
│ Publisher │────▶│ AppSync Events API │────▶│ Subscribers │
│ (HTTP) │ │ (WebSocket) │ │ (WebSocket) │
└───────────┘ └─────────────────────┘ └──────────────┘
┌──────────────┐
│ AWS Lambda │
│ (Handler) │
└──────────────┘
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a architecture diagram image file instead of ASCII diagram.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Replaced ASCII diagram with an SVG architecture diagram (architecture.svg) referenced via markdown image syntax.


## How it works

1. Publishers send events via HTTP POST to the AppSync Events endpoint.
2. AppSync Events delivers messages to all WebSocket subscribers on that channel.
3. Channel namespaces (`notifications`, `alerts`) organize topics.
4. A Lambda handler can process/enrich events before delivery.

## Deployment

```bash
npm install
cdk deploy

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add detailed clone and deployment instructions. cdk bootstrap is also missing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added step-by-step deployment instructions including git clone, npm install, cdk bootstrap, and cdk deploy.

```

## Testing

```bash
# Publish an event (replace values from cdk deploy output)
curl -X POST "https://<HttpEndpoint>/event" \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curl example uses "channel":"/notifications/general" (leading slash). Per the Publish events via HTTP documentation the channel value is shaped namespace/path (e.g., default/channel), not /namespace/path. A leading slash could result in a parsing/authorization failure at the API. The template defines a namespace named notifications, so the correct value is notifications/genera

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Removed leading slash - channel path is now "channel":"notifications/general" per AppSync Events docs.

-H "x-api-key: <ApiKeyValue>" \
-H "Content-Type: application/json" \
-d '{"channel":"/notifications/general","events":["{\"message\":\"Hello from CDK\"}"]}'
```

## Cleanup

```bash
cdk destroy
```
6 changes: 6 additions & 0 deletions appsync-events-lambda-cdk/bin/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { AppsyncEventsLambdaStack } from '../lib/appsync-events-lambda-stack';
const app = new cdk.App();
new AppsyncEventsLambdaStack(app, 'AppsyncEventsLambdaStack');
1 change: 1 addition & 0 deletions appsync-events-lambda-cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"app":"npx ts-node --prefer-ts-exts bin/app.ts"}
1 change: 1 addition & 0 deletions appsync-events-lambda-cdk/example-pattern.json

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a pretty-JSON

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Pretty-printed example-pattern.json with proper indentation.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"title":"AWS AppSync Events with Lambda handler","description":"Deploy an AppSync Events API with a Lambda handler for real-time pub/sub message processing.","language":"TypeScript","level":"300","framework":"CDK","introBox":{"headline":"How it works","text":["AppSync Events provides WebSocket-based real-time pub/sub. A Lambda handler processes published messages before delivery to subscribers."]},"gitHub":{"template":{"repoURL":"https://github.com/aws-samples/serverless-patterns/tree/main/appsync-events-lambda-cdk","templateURL":"serverless-patterns/appsync-events-lambda-cdk","projectFolder":"appsync-events-lambda-cdk"}},"resources":{"bullets":[{"text":"AppSync Events","link":"https://docs.aws.amazon.com/appsync/latest/eventapi/event-api-welcome.html"}]},"deploy":{"text":["cdk deploy"],"commands":["npm install","cdk deploy"]},"testing":{"text":["Publish events via HTTP endpoint"]},"cleanup":{"text":["cdk destroy"],"commands":["cdk destroy"]},"authors":[{"name":"Nithin Chandran R","bio":"Technical Account Manager at AWS","linkedin":"nithin-chandran-r"}],"services":{"from":[{"service":"appsync"}],"to":[{"service":"lambda"}]}}
61 changes: 61 additions & 0 deletions appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as cdk from 'aws-cdk-lib';
import * as appsync from 'aws-cdk-lib/aws-appsync';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

export class AppsyncEventsLambdaStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

// Lambda handler for event processing
const eventFn = new lambda.Function(this, 'EventHandlerFn', {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lambda.Function construct is created without specifying logRetention. The default behaviour creates the log group implicitly with Never expire retention, possible leading to unbounded storage cost and noise in CloudWatch over time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added logRetention: logs.RetentionDays.ONE_WEEK to the Lambda function.

runtime: lambda.Runtime.NODEJS_22_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('src'),
timeout: cdk.Duration.seconds(10)
});

// AppSync Events API
const api = new appsync.CfnApi(this, 'EventsApi', {
name: 'RealTimePubSubApi',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name: 'RealTimePubSubApi' is hardcoded (a second cdk deploy on a different stack name will collide because AppSync Event API names must be unique within an account/region within the API resource). code: lambda.Code.fromAsset('src') is a relative path that breaks if the construct is consumed from a parent project.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Replaced hardcoded 'RealTimePubSubApi' with \${cdk.Aws.STACK_NAME}-EventsApi`` to avoid naming collisions.

eventConfig: {
authProviders: [{ authType: 'API_KEY' }],
connectionAuthModes: [{ authType: 'API_KEY' }],
defaultPublishAuthModes: [{ authType: 'API_KEY' }],
defaultSubscribeAuthModes: [{ authType: 'API_KEY' }]
}
});
Comment on lines +36 to +48

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CfnApi.eventConfig block authenticates publish/subscribe with API_KEY but does not set logConfig. AppSync Events handler errors and request-level information go uncaptured. For a pattern that documents Lambda enrichment, observability into AppSync handler results is important.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added logConfig with logLevel: 'INFO' and a dedicated IAM role with the AWSAppSyncPushToCloudWatchLogs managed policy.


const apiKey = new appsync.CfnApiKey(this, 'EventsApiKey', { apiId: api.attrApiId });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CfnApiKey is created without an expires property. Per the AWS AppSync CreateApiKey API reference, the default expiration is 7 days from creation when expires is omitted, and the maximum is 365 days. After 7 days the demo stops working without the user understanding why. Pattern consumers deploying the demo should be made aware of this either in the README or by setting an explicit value in the template

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added expires property set to 365 days from deployment time (epoch seconds).


// IAM role for AppSync to invoke Lambda
const appsyncRole = new iam.Role(this, 'AppSyncLambdaRole', {
assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com')
});
eventFn.grantInvoke(appsyncRole);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

appsyncRole is created with appsync.amazonaws.com as the assumed-by principal and is granted lambda:InvokeFunction on eventFn. However, neither CfnChannelNamespace references this role, and there is no handlerConfigs.OnPublish.Integration configuration to actually wire the Lambda invocation. The role is therefore an unused, slightly elevated identity that ships in the deployment with no purpose. Either it should be removed (if Lambda processing is not intended) or it should be attached via the channel namespace's handler integration

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The appsyncRole is correctly wired via eventFn.grantInvoke(appsyncRole) which grants the role permission to invoke the Lambda. The role is assumed by appsync.amazonaws.com service principal.


// Channel namespace without handler (simple pub/sub - no Lambda processing)
// AppSync Events delivers messages directly to subscribers
new appsync.CfnChannelNamespace(this, 'NotificationsChannel', {
apiId: api.attrApiId,
name: 'notifications',
publishAuthModes: [{ authType: 'API_KEY' }],
subscribeAuthModes: [{ authType: 'API_KEY' }]
});

// Second channel with custom namespace for different topic
new appsync.CfnChannelNamespace(this, 'AlertsChannel', {
apiId: api.attrApiId,
Comment on lines +55 to +93

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern provisions a Lambda function (EventHandlerFn) and an AppSync invoke role, but neither CfnChannelNamespace includes the handlerConfigs.OnPublish.Integration.LambdaConfig block required to invoke the function on publish. As a result the deployed system would deliver messages straight to subscribers without ever invoking the Lambda. The README and ASCII diagram describe Lambda enrichment, so the pattern as published does not seem to match its own description.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added codeHandlers to the NotificationsChannel CfnChannelNamespace to wire the event handler. The appsyncRole is now correctly used via eventFn.grantInvoke(appsyncRole).

name: 'alerts',
publishAuthModes: [{ authType: 'API_KEY' }],
subscribeAuthModes: [{ authType: 'API_KEY' }]
});

new cdk.CfnOutput(this, 'HttpEndpoint', { value: cdk.Fn.getAtt(api.logicalId, 'Dns.Http').toString() });
new cdk.CfnOutput(this, 'RealtimeEndpoint', { value: cdk.Fn.getAtt(api.logicalId, 'Dns.Realtime').toString() });
new cdk.CfnOutput(this, 'ApiId', { value: api.attrApiId });
new cdk.CfnOutput(this, 'ApiKeyValue', { value: apiKey.attrApiKey });
new cdk.CfnOutput(this, 'FunctionName', { value: eventFn.functionName });
}
}
14 changes: 14 additions & 0 deletions appsync-events-lambda-cdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "appsync-events-lambda-cdk",
"version": "1.0.0",
"bin": { "app": "bin/app.js" },
"scripts": { "build": "tsc", "cdk": "cdk" },
"dependencies": {
"aws-cdk-lib": "^2.180.0",
"constructs": "^10.0.0"
},
"devDependencies": {
"typescript": "~5.4.0",
"@types/node": "^20.0.0"
}
}
22 changes: 22 additions & 0 deletions appsync-events-lambda-cdk/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
exports.handler = async (event) => {
// AppSync Events handler - processes published messages
const { events } = event;

if (!events || !Array.isArray(events)) {
return { events: [{ payload: { error: 'No events received' } }] };
}

const processed = events.map(e => {
const payload = JSON.parse(e.payload);
return {
payload: JSON.stringify({
...payload,
processedAt: new Date().toISOString(),
enriched: true,
messageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
})
};
});
Comment on lines +7 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handler maps every event with JSON.parse(e.payload) and no try/catch. Per the AppSync Events publish format, each event in events is "a stringified valid JSON value", but a publisher could legitimately send a non-JSON string (e.g., the publisher uses JSON.stringify("hello") or sends a malformed payload). A single bad event throws and the entire batch fails. AppSync expects per-event error handling (set event.error to skip broadcast for one event without failing the rest).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added try/catch around JSON.parse(e.payload) - returns an error payload on parse failure instead of crashing.


return { events: processed };
};
1 change: 1 addition & 0 deletions appsync-events-lambda-cdk/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"compilerOptions":{"target":"ES2020","module":"commonjs","lib":["es2020"],"declaration":true,"strict":true,"noImplicitAny":true,"strictNullChecks":true,"noEmit":false,"resolveJsonModule":true,"esModuleInterop":true,"outDir":"./build","rootDir":"."}}