<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Arpad Toth</title>
    <description>The latest articles on DEV Community by Arpad Toth (@arpadt).</description>
    <link>https://dev.to/arpadt</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F832031%2F3a048fdf-2aab-4327-a223-41dc063b0c97.jpeg</url>
      <title>DEV Community: Arpad Toth</title>
      <link>https://dev.to/arpadt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arpadt"/>
    <language>en</language>
    <item>
      <title>Implementing protected Lambda function URLs in user-facing applications</title>
      <dc:creator>Arpad Toth</dc:creator>
      <pubDate>Thu, 19 Feb 2026 08:41:57 +0000</pubDate>
      <link>https://dev.to/aws-builders/implementing-protected-lambda-function-urls-in-user-facing-applications-1f6p</link>
      <guid>https://dev.to/aws-builders/implementing-protected-lambda-function-urls-in-user-facing-applications-1f6p</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: When Lambda is configured to return streamed responses from IAM-protected function URLs, we can use Cognito identity pools to allow authenticated users to access the endpoint.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;



&lt;ul&gt;
&lt;li&gt;1. The scenario&lt;/li&gt;
&lt;li&gt;
2. Authorization in API Gateway vs function URL

&lt;ul&gt;
&lt;li&gt;2.1. API Gateway (REST type)&lt;/li&gt;
&lt;li&gt;2.2. Function URL&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;3. Architecture diagram&lt;/li&gt;

&lt;li&gt;

4. Architecture components

&lt;ul&gt;
&lt;li&gt;4.1. Cognito user pool&lt;/li&gt;
&lt;li&gt;4.2. Cognito identity pool&lt;/li&gt;
&lt;li&gt;4.3. Amplify&lt;/li&gt;
&lt;li&gt;4.4. Function URL&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;5. Considerations&lt;/li&gt;

&lt;li&gt;6. Summary&lt;/li&gt;

&lt;li&gt;

7. Further reading
&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. The scenario
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://arpadt.com/articles/dynamic-model-selection" rel="noopener noreferrer"&gt;dynamic model selection application&lt;/a&gt; currently uses an &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" rel="noopener noreferrer"&gt;API Gateway REST API&lt;/a&gt; as the entry point. It's because Bedrock returns &lt;strong&gt;streamed output&lt;/strong&gt; from the selected &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html" rel="noopener noreferrer"&gt;foundation model&lt;/a&gt;, and, as of this writing, only REST APIs support streamed responses back to the client.&lt;/p&gt;

&lt;p&gt;But API Gateway can be an overkill in some use cases. What if you just have a small function and don't want to configure an API Gateway?&lt;/p&gt;

&lt;p&gt;Good news, &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html" rel="noopener noreferrer"&gt;Lambda function URLs&lt;/a&gt; also support streamed responses!&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Authorization in API Gateway vs function URL
&lt;/h2&gt;

&lt;p&gt;Configuring the function URL for streamed responses is simple. It's only one line of code in &lt;a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html" rel="noopener noreferrer"&gt;CDK&lt;/a&gt; (see below).&lt;/p&gt;

&lt;p&gt;But how about authorization? How do you ensure that only authorized users can access the endpoint? Wouldn't it be better to still use an API Gateway?&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1. API Gateway (REST type)
&lt;/h3&gt;

&lt;p&gt;You can protect your API Gateway endpoints in a few &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html" rel="noopener noreferrer"&gt;different ways&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The most straightforward option is to use a &lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools.html" rel="noopener noreferrer"&gt;Cognito user pool&lt;/a&gt;. The flow is very simple. The user signs in, then the user pool issues &lt;strong&gt;tokens&lt;/strong&gt; (ID and access tokens), and API Gateway validates them.&lt;/p&gt;

&lt;p&gt;If you use a custom token or have custom claims, you can implement a &lt;strong&gt;Lambda authorizer&lt;/strong&gt; function. It decodes the token, runs some verification code and returns an &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html" rel="noopener noreferrer"&gt;IAM policy&lt;/a&gt; that allows or denies the invocation to API Gateway, depending on the token validation outcome.&lt;/p&gt;

&lt;p&gt;The third option is to use &lt;strong&gt;IAM authorization&lt;/strong&gt;, which is a great and secure way to control access to an API. It's a popular pattern to authorize internal, machine-to-machine requests. IAM validates the requests that must be signed with the &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html" rel="noopener noreferrer"&gt;Signature V4&lt;/a&gt; algorithm.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2. Function URL
&lt;/h3&gt;

&lt;p&gt;But let's say that you want the simplicity of &lt;strong&gt;function URLs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You have two options for your URL. It can be either a &lt;strong&gt;public&lt;/strong&gt; or an &lt;strong&gt;IAM protected&lt;/strong&gt; endpoint.&lt;/p&gt;

&lt;p&gt;Public function URLs are - surprise - available for everyone, so unless you're intentionally making it public, you'll eventually want to &lt;strong&gt;protect&lt;/strong&gt; the endpoint and authorize user requests.&lt;/p&gt;

&lt;p&gt;You can build custom logic in the Lambda function to authorize requests. It's a workable solution, but it fails to separate responsibilities, i.e., authorization and business logic. Instead, go with &lt;strong&gt;IAM authorization&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But here's the challenge. Your users need valid &lt;strong&gt;AWS credentials&lt;/strong&gt; included inside the SigV4 signature header when your application sends the request, otherwise, IAM denies it. That said, your users need &lt;code&gt;lambda:InvokeFunction&lt;/code&gt; and &lt;code&gt;lambda:InvokeFunctionUrl&lt;/code&gt; permissions to successfully call your IAM-protected function URL.&lt;/p&gt;

&lt;p&gt;Now, your application users around the world, Alices, Bobs, Cecils, etc., are &lt;strong&gt;not&lt;/strong&gt; very likely to be your AWS account users, and you don't want them to become ones. So how do you give them AWS credentials so that they can invoke the function URL backing your awesome application?&lt;/p&gt;

&lt;p&gt;A good practice in this scenario is to use Cognito &lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-identity.html" rel="noopener noreferrer"&gt;identity pools&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Architecture diagram
&lt;/h2&gt;

&lt;p&gt;This simplified diagram shows the workflow:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7xqne3ztbsjj23fxik9y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7xqne3ztbsjj23fxik9y.png" alt="IAM authorization with function URL" width="700" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the sake of simplicity, this solution has a Cognito user pool, but you can integrate any supported identity provider into your architecture. More on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Architecture components
&lt;/h2&gt;

&lt;p&gt;Let's review the steps and architecture components.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. Cognito user pool
&lt;/h3&gt;

&lt;p&gt;The user pool contains user authentication data, like username, password, sign-in, and sign-up preferences.&lt;/p&gt;

&lt;p&gt;You can also create a &lt;strong&gt;user group&lt;/strong&gt; in the user pool, and add users to it. User groups are actually &lt;strong&gt;access control&lt;/strong&gt; groups since you can assign different &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html" rel="noopener noreferrer"&gt;IAM roles&lt;/a&gt; to different groups. Users belonging to the same groups get the same permissions.&lt;/p&gt;

&lt;p&gt;After users successfully authenticate with their username and password (or other sign-in options you configure for them in the user pool), Cognito returns an &lt;strong&gt;ID token&lt;/strong&gt; to the application. The ID token includes the user's group membership and the name of the corresponding &lt;strong&gt;role&lt;/strong&gt; assigned to the user group.&lt;/p&gt;

&lt;p&gt;This is where the user pool's responsibility ends in this solution. In this example, each user belongs to only one Cognito group. A future post might cover a scenario when users belong to multiple groups.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2. Cognito identity pool
&lt;/h3&gt;

&lt;p&gt;The next step and the solution's critical component is the &lt;strong&gt;identity pool&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The ID token issued by the user pool includes the &lt;strong&gt;IAM role&lt;/strong&gt; attached to the group the user is assigned to. The application receives this token from Cognito and calls the identity pool with it (&lt;code&gt;GetId&lt;/code&gt; and &lt;code&gt;GetCredentialsForIdentity&lt;/code&gt; APIs). After the identity pool verifies the token, it retrieves &lt;strong&gt;temporary AWS credentials&lt;/strong&gt; from &lt;a href="https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html" rel="noopener noreferrer"&gt;STS&lt;/a&gt;, and returns them to the application.&lt;/p&gt;

&lt;p&gt;The application (in this case, a &lt;a href="https://react.dev/" rel="noopener noreferrer"&gt;React&lt;/a&gt; app) adds the credentials to the SigV4 signature, signs the request, and attaches the signed headers to the function URL call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// React component&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SignatureV4&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws-sdk/signature-v4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Sha256&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws-crypto/sha256-js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HttpRequest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@smithy/protocol-http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ... more component code&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SignatureV4&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// easy to get them with Amplify&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;REGION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signedRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LAMBDA_FUNCTION_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;signedRequest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ... more code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.3. Amplify
&lt;/h3&gt;

&lt;p&gt;You can use the &lt;a href="https://docs.amplify.aws/react/" rel="noopener noreferrer"&gt;Amplify&lt;/a&gt; React package to simplify workflows, such as sign-in logic or retrieving AWS credentials from the session object.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;aws-amplify/auth&lt;/code&gt; package includes the &lt;code&gt;fetchAuthSession&lt;/code&gt; method, where the AWS credentials can be obtained with just two lines of code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchAuthSession&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;credentials&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full code is available on my GitHub page in &lt;a href="https://github.com/arpadt/aip-c01-domain-1-task-1_2-dynamic-model-selection" rel="noopener noreferrer"&gt;this repo&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4. Function URL
&lt;/h3&gt;

&lt;p&gt;You'll need to configure &lt;code&gt;authType&lt;/code&gt; to &lt;code&gt;AWS_IAM&lt;/code&gt; for &lt;strong&gt;IAM authorization&lt;/strong&gt; on the function URL.&lt;/p&gt;

&lt;p&gt;Also, since the response is streamed, I  configured the URL accordingly.&lt;/p&gt;

&lt;p&gt;Third, since the function URL is invoked from a browser via a React application, I also configured the CORS settings.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;modelAbstractionFnForUrl&lt;/code&gt; is a Lambda function construct, so you can enable the URL with the &lt;code&gt;addFunctionUrl&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modelAbstractionFnUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;modelAbstractionFnForUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addFunctionUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// configure IAM authorization&lt;/span&gt;
  &lt;span class="na"&gt;authType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FunctionUrlAuthType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AWS_IAM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// enable browsers to call the function URL&lt;/span&gt;
  &lt;span class="c1"&gt;// narrow it down to the actual domain in production&lt;/span&gt;
  &lt;span class="na"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;allowedOrigins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;allowedMethods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HttpMethod&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALL&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;allowedHeaders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;maxAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// configure STREAMED response&lt;/span&gt;
  &lt;span class="na"&gt;invokeMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InvokeMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RESPONSE_STREAM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now have a protected function URL that returns streamed responses! After your users successfully log in to the application, they can send prompts to the foundation model via the URL.&lt;/p&gt;

&lt;p&gt;The entire code (infra and application) can be found in the &lt;a href="https://github.com/arpadt/aip-c01-domain-1-task-1_2-dynamic-model-selection" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Considerations
&lt;/h2&gt;

&lt;p&gt;As stated above, the use case largely influences whether function URLs are a feasible solution. If you need the features of API Gateway, you don't necessarily need an identity pool. But you can design a similar architecture for API Gateway. In that case, you'll need to change the IAM role's permissions attached to the user group and the SigV4 code from &lt;code&gt;lambda&lt;/code&gt; to &lt;code&gt;execute-api&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As always, the solution presented in this post is not production-ready, and its feasibility should always be assessed against your use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Summary
&lt;/h2&gt;

&lt;p&gt;Lambda function URLs support streaming responses and can be securely restricted to authenticated users via Cognito user pools and identity pools.&lt;/p&gt;

&lt;p&gt;The client signs requests using SigV4, which includes the temporary AWS credentials, based on IAM roles attached to Cognito user pool groups.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Further reading
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://arpadt.com/articles/cognito-groups-iam-roles" rel="noopener noreferrer"&gt;Controlling access to resources with Cognito groups and IAM roles - Authenticated users in Cognito Identity pools, Part 1&lt;/a&gt; - Something similar with DynamoDB, and a bit more details on how the identity pool behaves&lt;/p&gt;

</description>
      <category>lambda</category>
      <category>cognito</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Building a Dynamic Bedrock Model Selection Serverless Application</title>
      <dc:creator>Arpad Toth</dc:creator>
      <pubDate>Thu, 05 Feb 2026 10:16:46 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-a-dynamic-bedrock-model-selection-serverless-application-2phh</link>
      <guid>https://dev.to/aws-builders/building-a-dynamic-bedrock-model-selection-serverless-application-2phh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: With the help of Amazon Bedrock, AppConfig, Lambda and API Gateway, we can create a minimalist application that dynamically selects foundation models based on use cases.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;



&lt;ul&gt;
&lt;li&gt;1. Context&lt;/li&gt;
&lt;li&gt;2. The challenge&lt;/li&gt;
&lt;li&gt;3. Architecture diagram&lt;/li&gt;
&lt;li&gt;
4. Code

&lt;ul&gt;
&lt;li&gt;4.1. Evaluation script&lt;/li&gt;
&lt;li&gt;4.2. Infrastructure code&lt;/li&gt;
&lt;li&gt;4.3. Backend code&lt;/li&gt;
&lt;li&gt;4.4. Front-end code&lt;/li&gt;
&lt;li&gt;4.5. GitHub&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

5. Considerations and limitations

&lt;ul&gt;
&lt;li&gt;5.1. Considerations&lt;/li&gt;
&lt;li&gt;5.2. Limitations&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;6. Summary&lt;/li&gt;

&lt;li&gt;

7. Further reading
&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Context
&lt;/h2&gt;

&lt;p&gt;If you are studying for the &lt;a href="https://aws.amazon.com/certification/certified-generative-ai-developer-professional/" rel="noopener noreferrer"&gt;AWS Certified Generative AI Developer - Professional&lt;/a&gt; exam, you might have considered the &lt;a href="https://skillbuilder.aws/learning-plan/9VXVGYT38G/exam-prep-plan-aws-certified-generative-ai-developer--professional-aipc01--english/4SCMN2659K" rel="noopener noreferrer"&gt;Exam Prep Plan&lt;/a&gt; in &lt;strong&gt;AWS Skill Builder&lt;/strong&gt; as a resource.&lt;/p&gt;

&lt;p&gt;The Exam Prep Plan discusses each domain and the corresponding tasks one by one. Tasks in many domains include &lt;strong&gt;bonus assignment&lt;/strong&gt; challenges after covering the required knowledge.&lt;/p&gt;

&lt;p&gt;This post presents a solution to the first part of the bonus  assignment described in &lt;strong&gt;Task 1.2&lt;/strong&gt; of the &lt;a href="https://skillbuilder.aws/learn/GT2P1KK636/domain-1-review-aws-certified-generative-ai-developer--professional-aipc01-english/4GWFTZBZ74?parentId=4SCMN2659K" rel="noopener noreferrer"&gt;Domain 1 Review&lt;/a&gt; section.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The challenge
&lt;/h2&gt;

&lt;p&gt;The assignment is about creating an AI application that dynamically selects &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html" rel="noopener noreferrer"&gt;foundation models in Bedrock&lt;/a&gt; based on use cases.&lt;/p&gt;

&lt;p&gt;The task is quite complex with many moving parts, so I decided to implement only the dynamic model selection logic for now.&lt;/p&gt;

&lt;p&gt;Selecting the right model for a specific use case is critical for cost saving, latency optimization or response correctness. For example, there's no point in using a large, more expensive model for a simple text summarization task, which is better suited to a small text model.&lt;/p&gt;

&lt;p&gt;But what if our application covers multiple use cases?&lt;/p&gt;

&lt;p&gt;One option is to implement &lt;strong&gt;dynamic model selection&lt;/strong&gt; logic which automatically chooses the right model for a given use case.&lt;/p&gt;

&lt;p&gt;This post describes a very basic model selection application using serverless AWS resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Architecture diagram
&lt;/h2&gt;

&lt;p&gt;The architecture isn't very complex:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcboy0fczm8eri5206pst.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcboy0fczm8eri5206pst.png" alt="Simple model selection architecture" width="662" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Users create a prompt in the UI and select a use case, like &lt;strong&gt;balanced&lt;/strong&gt; or &lt;strong&gt;performance optimized&lt;/strong&gt;, from a dropdown list.&lt;/p&gt;

&lt;p&gt;Both the prompt and the selected use case are sent to an &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" rel="noopener noreferrer"&gt;API Gateway REST API&lt;/a&gt; endpoint, which is integrated with a &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-integrations.html" rel="noopener noreferrer"&gt;Lambda function&lt;/a&gt;. The function retrieves the &lt;strong&gt;model selection strategy&lt;/strong&gt; configuration object from &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-integrations.html" rel="noopener noreferrer"&gt;AppConfig&lt;/a&gt;, finds the best model for the use case and invokes it with the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Code
&lt;/h2&gt;

&lt;p&gt;Let's highlight some key elements from the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. Evaluation script
&lt;/h3&gt;

&lt;p&gt;To select the right model for a use case, we can evaluate and compare how the models perform.&lt;/p&gt;

&lt;p&gt;We can use multiple methods and services to assess model performance, including Bedrock's &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/evaluation.html" rel="noopener noreferrer"&gt;model evaluation&lt;/a&gt; jobs.&lt;/p&gt;

&lt;p&gt;The solution in this post follows the bonus assignment suggestion to use &lt;strong&gt;custom&lt;/strong&gt; assessment. I borrowed the main logic from the &lt;a href="https://skillbuilder.aws/learn/GT2P1KK636/domain-1-review-aws-certified-generative-ai-developer--professional-aipc01-english/4GWFTZBZ74?parentId=4SCMN2659K" rel="noopener noreferrer"&gt;Exam Prep Plan&lt;/a&gt;, made some adjustments, and added test cases and metrics.&lt;/p&gt;

&lt;p&gt;Each &lt;strong&gt;test case&lt;/strong&gt; contains a &lt;strong&gt;question&lt;/strong&gt; and a related &lt;strong&gt;ground truth&lt;/strong&gt; answer. The evaluation script compares model responses to the provided ground-truth answers and scores the results in latency, similarity, and cost categories.&lt;/p&gt;

&lt;p&gt;I made some modifications to the original code. As an example, the evaluation code provided in the Prep Plan includes a basic keyword check, which I replaced with &lt;a href="https://www.geeksforgeeks.org/dbms/cosine-similarity/" rel="noopener noreferrer"&gt;cosine similarity&lt;/a&gt; to capture the semantic similarities between the ground truth and the model response.&lt;/p&gt;

&lt;p&gt;I also changed the provided normalization method to &lt;a href="https://statisticseasily.com/glossario/what-is-min-max-normalization-explained/" rel="noopener noreferrer"&gt;min-max normalization&lt;/a&gt; to ensure all scores fall between 0 and 1. This way, when the script calculates the &lt;strong&gt;weights&lt;/strong&gt; for each use case, the difference between lower and higher scores will be greater and more readable to the human eye.&lt;/p&gt;

&lt;p&gt;Ultimately, the script selects the models that achieve the highest scores in each assessment category and pairs the model IDs with the corresponding use cases.&lt;/p&gt;

&lt;p&gt;The last step is to create a &lt;code&gt;JSON&lt;/code&gt; file with the model selection strategy configurations, which is then uploaded to AppConfig.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2. Infrastructure code
&lt;/h3&gt;

&lt;p&gt;Contrary to the suggested bonus assignment solution, I wanted to use &lt;strong&gt;streaming&lt;/strong&gt; responses in the application. This way, users receive the response token by token as the model generates them. And it looks cool, too.&lt;/p&gt;

&lt;p&gt;I used the &lt;a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html" rel="noopener noreferrer"&gt;CDK&lt;/a&gt; in &lt;strong&gt;TypeScript&lt;/strong&gt; to create application resources.&lt;/p&gt;

&lt;p&gt;First, I want to configure streaming responses in both API Gateway and Lambda. Let's start with API Gateway. For REST APIs, the stream configuration lives inside the &lt;strong&gt;method&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// create the "api" before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateResource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;generate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;generateResource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;apigateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LambdaIntegration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelAbstractionFn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;responseTransferMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apigateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResponseTransferMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STREAM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// options&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We set &lt;code&gt;STREAM&lt;/code&gt; on the &lt;code&gt;responseTranserMode&lt;/code&gt; property (which defaults to &lt;code&gt;BUFFER&lt;/code&gt;) to enable API Gateway to stream the response to the client. AWS &lt;a href="https://aws.amazon.com/blogs/compute/building-responsive-apis-with-amazon-api-gateway-response-streaming/" rel="noopener noreferrer"&gt;introduced this feature&lt;/a&gt; a few weeks ago, and it comes in handy for generative AI and other, long-processing applications.&lt;/p&gt;

&lt;p&gt;We can combine the API Gateway streaming response configuration with the &lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/06/amazon-api-gateway-integration-timeout-limit-29-seconds/" rel="noopener noreferrer"&gt;increased timeout limit&lt;/a&gt; to ensure the client receives the response even if the model takes more than 29 seconds to create the output. The &lt;code&gt;timeout&lt;/code&gt; setting allows up to &lt;strong&gt;15 minutes&lt;/strong&gt; instead of the standard 29 seconds for &lt;strong&gt;regional and private REST APIs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;As of this writing, increased timeout and streaming responses in API Gateway HTTP APIs are not supported.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3. Backend code
&lt;/h3&gt;

&lt;p&gt;The code sample in the Exam Prep Plan is written in Python. But I wanted to use &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html" rel="noopener noreferrer"&gt;Lambda function response streaming&lt;/a&gt;! Currently, only the &lt;a href="https://nodejs.org/en" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; managed runtime supports this feature, so I took a deep breath and migrated the original Python code to &lt;strong&gt;TypeScript&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It wasn't just a lift-and-shift job, since I made some minor changes as well. Instead of using the SDK and the (deprecated) &lt;code&gt;GetConfiguration&lt;/code&gt; API, the &lt;code&gt;model_abstraction.ts&lt;/code&gt; Lambda handler function uses the &lt;a href="https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions.html" rel="noopener noreferrer"&gt;AppConfig Agent Lambda extension&lt;/a&gt; to retrieve the configuration from AppConfig.&lt;/p&gt;

&lt;p&gt;The extension is added as a &lt;strong&gt;layer&lt;/strong&gt; to the function and we can include it in the infra code like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modelAbstractionFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambdaNodejs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NodejsFunction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ModelAbstractionFunction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// .. other configurations&lt;/span&gt;
    &lt;span class="na"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LayerVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromLayerVersionArn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AppConfigLayer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// modify the URL to match your region&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;arn:aws:lambda:eu-central-1:066940009817:layer:AWS-AppConfig-Extension:261&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;APP_CONFIG_APPLICATION_NAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;appConfigApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;APP_CONFIG_ENVIRONMENT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;appConfigEnvironment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;APP_CONFIG_CONFIGURATION&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;configProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you deploy the stack to a &lt;strong&gt;different region&lt;/strong&gt;, you'll need to select the &lt;a href="https://docs.aws.amazon.com/appconfig/latest/userguide/appconfig-integration-lambda-extensions-versions.html" rel="noopener noreferrer"&gt;correct extension ARN&lt;/a&gt; for that region.&lt;/p&gt;

&lt;p&gt;The function's &lt;strong&gt;execution role&lt;/strong&gt; also needs the &lt;code&gt;appconfig:GetLatestConfiguration&lt;/code&gt; and &lt;code&gt;appconfig:StartConfigurationSession&lt;/code&gt; permissions, since AWS recommends these APIs over &lt;code&gt;GetConfiguration&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The extension connects to AppConfig through port &lt;code&gt;2772&lt;/code&gt;. Here's my implementation for the &lt;code&gt;retrieveConfig&lt;/code&gt; function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AppConfigProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;retrieveConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;configProps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AppConfigProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IncomingMessage&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;application&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;configuration&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;configProps&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`http://localhost:2772/applications/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;application&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/environments/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/configurations/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The required values, &lt;code&gt;application&lt;/code&gt;, &lt;code&gt;environment&lt;/code&gt; and &lt;code&gt;configuration&lt;/code&gt;, are provided via environment variables.&lt;/p&gt;

&lt;p&gt;The function then parses the input event to get the &lt;code&gt;prompt&lt;/code&gt; and &lt;code&gt;use_case&lt;/code&gt; properties, extracts the &lt;strong&gt;model ID&lt;/strong&gt; for the use case from the retrieved model selection strategy configuration, and calls the &lt;code&gt;InvokeModelWithResponseStream&lt;/code&gt; Bedrock API. As chunks start arriving from Bedrock, we keep writing them to the response stream using &lt;code&gt;responseStream.write&lt;/code&gt;. The response stream is created with the built-in &lt;code&gt;awslambda.HttpResponseStream.from()&lt;/code&gt; method.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4. Front-end code
&lt;/h3&gt;

&lt;p&gt;The minimalistic UI is also written in TypeScript using &lt;a href="https://react.dev/" rel="noopener noreferrer"&gt;React&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We must ensure that the component (&lt;code&gt;App.tsx&lt;/code&gt;) handles streaming responses and processes the incoming chunks. The user will now see the response appear on the screen gradually as pieces arrive from the backend.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.5. GitHub
&lt;/h3&gt;

&lt;p&gt;The code is available on my GitHub page in &lt;a href="https://github.com/arpadt/aip-c01-domain-1-task-1_2-dynamic-model-selection" rel="noopener noreferrer"&gt;this repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Considerations and limitations
&lt;/h2&gt;

&lt;p&gt;The project's purpose is to present an idea. The solution is definitely &lt;strong&gt;not&lt;/strong&gt; production-ready!&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1. Considerations
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Nova models&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I use &lt;a href="https://aws.amazon.com/nova/" rel="noopener noreferrer"&gt;Amazon Nova&lt;/a&gt; models in the application. Feel free to add models from different vendors. In that case, you may need to modify the Lambda function's code because the inference parameter configurations can differ.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic assessments&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The custom model assessment presented in the code is very basic. Real-life assessments should consider more metrics and more complex business logic. Again, the idea is to have &lt;strong&gt;some&lt;/strong&gt; basic metrics that can be used the generate a model selection strategy file, then select the model based on the configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AppConfig&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Using AppConfig isn't mandatory, although it's a good practice. It lets us modify the configuration without redeploying the application. This way, we delegate individual responsibilities to dedicated resources, following the separation of concerns principle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluation script&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We can also run the evaluation script in the cloud instead of locally on the laptop. Since it takes time to invoke all models with all test cases, an &lt;strong&gt;asynchronous, event-driven&lt;/strong&gt; workflow would work well here. I didn't implement this pattern, as the project's focus was the model selection logic itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Streaming&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most models support &lt;strong&gt;streaming&lt;/strong&gt; operations, but not all of them. You can &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html" rel="noopener noreferrer"&gt;verify&lt;/a&gt; if your selected models support this feature before adding them to the pool.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2. Limitations
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The application doesn't implement any authentication or access control for the API. Feel free to add a &lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools.html" rel="noopener noreferrer"&gt;Cognito user pool&lt;/a&gt; or a similar service to protect the endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt filtering&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Prompt inputs aren't validated, and sensitive data aren't handled. You should implement this, for example, by using &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-sensitive-filters.html" rel="noopener noreferrer"&gt;Bedrock Guardrails&lt;/a&gt; to filter model input and output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production environment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Other important production components, such as scaling and monitoring/observability, aren't implemented either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correctness&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As pointed out above, the bonus assignment code sample counts keywords in the model response and compares them to the ground truth. This method fails when the response is semantically correct but uses different wording from the ground truth, which is why I replaced it with a basic semantic similarity calculation in the code.&lt;/p&gt;

&lt;p&gt;In real-world applications, both methods, individually and combined (in a hybrid approach), can have their place. For example, legal text usually requires exact word matching, so pure semantic similarity may not work well there. Always consider your specific use case when choosing an accuracy measurement method.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Summary
&lt;/h2&gt;

&lt;p&gt;Task 1.2 in Domain 1 is about selecting and configuring foundation models. The solution described here implements a basic model assessment and selection logic that generates the model selection strategy configuration file.&lt;/p&gt;

&lt;p&gt;AppConfig stores the file, while an API Gateway REST API provides the entry point to the application. REST APIs now support streaming responses, which, together with Lambda's support for streaming, are handy features for generative AI applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Further reading
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://skillbuilder.aws/learning-plan/9VXVGYT38G/exam-prep-plan-aws-certified-generative-ai-developer--professional-aipc01--english/4SCMN2659K" rel="noopener noreferrer"&gt;Exam Prep Plan: AWS Certified Generative AI Developer - Professional (AIP-C01 - English)&lt;/a&gt; - Exam preparation plan&lt;/p&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/blogs/apn/scale-ai-application-in-production-build-a-fault-tolerant-ai-gateway-with-snapsoft/" rel="noopener noreferrer"&gt;Scale AI application in production: Build a fault-tolerant AI gateway with SnapSoft&lt;/a&gt; - A more robust architecture for production&lt;/p&gt;

</description>
      <category>aws</category>
      <category>bedrock</category>
      <category>serverless</category>
      <category>genai</category>
    </item>
    <item>
      <title>Building an end-to-end insurance claims app with Amazon Bedrock</title>
      <dc:creator>Arpad Toth</dc:creator>
      <pubDate>Thu, 29 Jan 2026 08:56:46 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-an-end-to-end-insurance-claims-app-with-amazon-bedrock-4o07</link>
      <guid>https://dev.to/aws-builders/building-an-end-to-end-insurance-claims-app-with-amazon-bedrock-4o07</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Task 1.1 assignment in the AWS Skill Builder Generative AI Developer Professional Exam Prep Plan asks you to build a simple insurance claims application that uses Bedrock models to extract and summarize claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;



&lt;ul&gt;
&lt;li&gt;1. Context&lt;/li&gt;
&lt;li&gt;2. The challenge&lt;/li&gt;
&lt;li&gt;
3. Architecture diagram

&lt;ul&gt;
&lt;li&gt;3.1. Uploading the claim&lt;/li&gt;
&lt;li&gt;3.2. Extracting the information&lt;/li&gt;
&lt;li&gt;3.3. Summarizing the claim&lt;/li&gt;
&lt;li&gt;3.4. Returning the response to the client&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

4. Code

&lt;ul&gt;
&lt;li&gt;4.1. Application code&lt;/li&gt;
&lt;li&gt;4.2. Infrastructure code&lt;/li&gt;
&lt;li&gt;4.3. Front end&lt;/li&gt;
&lt;li&gt;4.4. GitHub&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;5. Considerations and limitations&lt;/li&gt;

&lt;li&gt;6. Summary&lt;/li&gt;

&lt;li&gt;

7. Further reading
&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Context
&lt;/h2&gt;

&lt;p&gt;If you are studying for the &lt;a href="https://aws.amazon.com/certification/certified-generative-ai-developer-professional/" rel="noopener noreferrer"&gt;AWS Certified Generative AI Developer - Professional&lt;/a&gt; exam, you might have considered the &lt;a href="https://skillbuilder.aws/learning-plan/9VXVGYT38G/exam-prep-plan-aws-certified-generative-ai-developer--professional-aipc01--english/4SCMN2659K" rel="noopener noreferrer"&gt;Exam Prep Plan&lt;/a&gt; in &lt;strong&gt;AWS Skill Builder&lt;/strong&gt; as a resource.&lt;/p&gt;

&lt;p&gt;The Exam Prep Plan discusses each domain and the tasks within the domains one by one. Each task in a domain contains a &lt;strong&gt;bonus assignment&lt;/strong&gt; challenge after covering the required knowledge.&lt;/p&gt;

&lt;p&gt;This post presents a solution to the assignment described in &lt;strong&gt;Task 1.1&lt;/strong&gt; of the &lt;a href="https://skillbuilder.aws/learn/GT2P1KK636/domain-1-review-aws-certified-generative-ai-developer--professional-aipc01-english/4GWFTZBZ74?parentId=4SCMN2659K" rel="noopener noreferrer"&gt;Domain 1 Review&lt;/a&gt; section.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The challenge
&lt;/h2&gt;

&lt;p&gt;The assignment is about creating a simple &lt;strong&gt;insurance claims&lt;/strong&gt; application built around the task's exam requirements.&lt;/p&gt;

&lt;p&gt;The app accepts insurance claims from users and summarizes their most important parts using &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html" rel="noopener noreferrer"&gt;Amazon Bedrock models&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The flow should include two Bedrock calls: the first to &lt;strong&gt;extract&lt;/strong&gt; the most important pieces from the claim, and the second to summarize it in natural language.&lt;/p&gt;

&lt;p&gt;The assignment describes some optional elements, several of which are implemented in this solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Architecture diagram
&lt;/h2&gt;

&lt;p&gt;The diagram below shows the entire flow of the solution, which includes some additional, non-required elements.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz28h694rrmcgw4po4yum.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz28h694rrmcgw4po4yum.png" alt="Simple insurance claim application" width="800" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1. Uploading the claim
&lt;/h3&gt;

&lt;p&gt;When the user clicks the &lt;code&gt;Upload Claim&lt;/code&gt; button in the UI, an &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" rel="noopener noreferrer"&gt;API Gateway REST API&lt;/a&gt; endpoint accepts the request. A &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-integrations.html" rel="noopener noreferrer"&gt;Lambda function&lt;/a&gt; generates a &lt;strong&gt;presigned URL&lt;/strong&gt;, which the client then uses to upload the file to the &lt;strong&gt;claims&lt;/strong&gt; &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/creating-buckets-s3.html" rel="noopener noreferrer"&gt;S3 bucket&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2. Extracting the information
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;claims processor&lt;/strong&gt; Lambda function receives an &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/EventNotifications.html#grant-destinations-permissions-to-s3" rel="noopener noreferrer"&gt;Event Notification&lt;/a&gt; from S3 when a new object is uploaded to the bucket.&lt;/p&gt;

&lt;p&gt;The function then calls the &lt;code&gt;InvokeModel&lt;/code&gt; API in Bedrock to use a small text model, &lt;strong&gt;Amazon Nova Micro&lt;/strong&gt;, to extract the key properties from the claim. Cost efficiency is key here. This task isn't complex, so it's more cost-effective to use a smaller model. The prompt includes the required &lt;code&gt;JSON&lt;/code&gt; response format.&lt;/p&gt;

&lt;p&gt;Optionally, you can apply a &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-sensitive-filters.html" rel="noopener noreferrer"&gt;Bedrock Guardrail&lt;/a&gt; to the LLM call to remove sensitive information from both the model input and output.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3. Summarizing the claim
&lt;/h3&gt;

&lt;p&gt;The next step is to ask the model to &lt;strong&gt;summarize&lt;/strong&gt; the claim information based on the extracted data.&lt;/p&gt;

&lt;p&gt;If you want the fictional insurance company's proprietary policy information to be considered in the model's response, you can use AWS's managed RAG service, &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/knowledge-base.html" rel="noopener noreferrer"&gt;Bedrock Knowledge Base&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This solution includes a knowledge base with a &lt;strong&gt;source&lt;/strong&gt; S3 bucket that stores the policy files, an &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/titan-embedding-models.html" rel="noopener noreferrer"&gt;embedding model&lt;/a&gt; that converts documents into vectors, and an &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-vectors.html" rel="noopener noreferrer"&gt;S3 Vectors&lt;/a&gt; vector bucket to store the vectors.&lt;/p&gt;

&lt;p&gt;The claims processor function uses the &lt;code&gt;RetrieveAndGenerate&lt;/code&gt; Bedrock API to add the relevant policy documentation parts to the summarization model's input.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.4. Returning the response to the client
&lt;/h3&gt;

&lt;p&gt;The claims processor Lambda function &lt;strong&gt;persists&lt;/strong&gt; the summary returned by the summarization model, as well as some other data like claim ID, model ID, and the time it took the model to summarize the claim, to a &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithTables.html" rel="noopener noreferrer"&gt;DynamoDB table&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The next step is to capture the &lt;strong&gt;data changes&lt;/strong&gt; (e.g., new items in the table) through &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html" rel="noopener noreferrer"&gt;DynamoDB Streams&lt;/a&gt;. The stream handler Lambda function takes the new item from the stream event and forwards it to an &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api.html" rel="noopener noreferrer"&gt;API Gateway WebSocket API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The client connected to the WebSocket API should receive the summary within 2–3 seconds, giving the user real-time confirmation of their claim submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Code
&lt;/h2&gt;

&lt;p&gt;I split the code into three parts (all in the same repo for convenience).&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. Application code
&lt;/h3&gt;

&lt;p&gt;The application code, written in &lt;strong&gt;Python&lt;/strong&gt;, largely relies on the code samples provided by the training material in Skill Builder.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2. Infrastructure code
&lt;/h3&gt;

&lt;p&gt;I created and deployed the resources using &lt;a href="https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html" rel="noopener noreferrer"&gt;CDK&lt;/a&gt; in &lt;strong&gt;TypeScript&lt;/strong&gt; for easy deployment and stack deletion.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3. Front end
&lt;/h3&gt;

&lt;p&gt;The minimalistic UI is also written in TypeScript. The web interface uses &lt;a href="https://react.dev/" rel="noopener noreferrer"&gt;React&lt;/a&gt; instead of the Flask approach recommended in the Skill Builder assignment.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4. GitHub
&lt;/h3&gt;

&lt;p&gt;Deployment and app-running information is available in &lt;a href="https://github.com/arpadt/aip-c01-domain-1-task-1_1-insurance-claims" rel="noopener noreferrer"&gt;this GitHub repo&lt;/a&gt; if you want to take a look.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Considerations and limitations
&lt;/h2&gt;

&lt;p&gt;The project is &lt;strong&gt;not&lt;/strong&gt; production-ready! It's simply a possible solution to the assignment and lacks some important features. See the &lt;a href="https://github.com/arpadt/aip-c01-domain-1-task-1_1-insurance-claims/blob/master/README.md" rel="noopener noreferrer"&gt;README&lt;/a&gt; for more information.&lt;/p&gt;

&lt;p&gt;The application in its current form only accepts &lt;code&gt;.txt&lt;/code&gt; files. It assumes that claims are already converted from PDF, DOC, or image files. Handling multiple file extensions is a possible extension to the project. You could add packages in the code, or use Bedrock models or &lt;a href="https://docs.aws.amazon.com/textract/latest/dg/what-is.html" rel="noopener noreferrer"&gt;Textract&lt;/a&gt; to extract information from the uploaded claims. The solution's stack doesn't include any of these since the purpose of the project was different.&lt;/p&gt;

&lt;p&gt;Also, feel free to add more checks to the guardrail, protect the API endpoint with a token, use different models, or keep the WebSocket connection alive in the client with some custom logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Summary
&lt;/h2&gt;

&lt;p&gt;That's it! Above is a solution to the Task 1 bonus assignment in Domain 1 in the Skill Builder Exam Prep Plan for the Generative AI Developer Professional certification exam.&lt;/p&gt;

&lt;p&gt;The application extracts and summarizes insurance claims and uses Bedrock foundation models, Bedrock Knowledge Bases, and Bedrock Guardrails.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Further reading
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://skillbuilder.aws/learning-plan/9VXVGYT38G/exam-prep-plan-aws-certified-generative-ai-developer--professional-aipc01--english/4SCMN2659K" rel="noopener noreferrer"&gt;Exam Prep Plan: AWS Certified Generative AI Developer - Professional (AIP-C01 - English)&lt;/a&gt; - Exam preparation plan&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>bedrock</category>
      <category>generativeai</category>
    </item>
    <item>
      <title>16 hands-on exercises to prepare for the AWS Certified CloudOps Engineer - Associate certification exam</title>
      <dc:creator>Arpad Toth</dc:creator>
      <pubDate>Thu, 18 Dec 2025 12:08:29 +0000</pubDate>
      <link>https://dev.to/aws-builders/16-hands-on-exercises-to-prepare-for-the-aws-certified-cloudops-engineer-associate-certification-4c7</link>
      <guid>https://dev.to/aws-builders/16-hands-on-exercises-to-prepare-for-the-aws-certified-cloudops-engineer-associate-certification-4c7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: The AWS Certified CloudOps Engineer - Associate certification exam is a practical exam. Combining theory with hands-on experience will increase your chances of achieving a pass result.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;



&lt;ul&gt;
&lt;li&gt;1. About the exam&lt;/li&gt;
&lt;li&gt;
2. Considerations and prerequisites

&lt;ul&gt;
&lt;li&gt;2.1. AWS Organizations&lt;/li&gt;
&lt;li&gt;2.2. Domain name&lt;/li&gt;
&lt;li&gt;2.3. Cost considerations&lt;/li&gt;
&lt;li&gt;2.4. On-premise environment&lt;/li&gt;
&lt;li&gt;2.5. Bulk exercises&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;3. Disclaimer&lt;/li&gt;

&lt;li&gt;4. The exercises&lt;/li&gt;

&lt;li&gt;5. Summary&lt;/li&gt;

&lt;li&gt;

6. Further reading and learning
&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. About the exam
&lt;/h2&gt;

&lt;p&gt;The popular AWS Certified SysOps Administrator - Associate certification was renamed as &lt;strong&gt;AWS Certified CloudOps Engineer - Associate (SOA-C03)&lt;/strong&gt; recently. The new gown reflects the change in both the available AWS technologies and job roles.&lt;/p&gt;

&lt;p&gt;As such, new services were added to the exam topic list, and AWS reorganized some of the task statements.&lt;/p&gt;

&lt;p&gt;The certification exam now heavily tests your deployment, operation, and maintenance skills both in single and multi-account environments. As it has been the case with the previous version, the renamed certification has a large focus on &lt;strong&gt;settings&lt;/strong&gt;, &lt;strong&gt;configurations&lt;/strong&gt; and &lt;strong&gt;automations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I believe that learning only theory or cramming these configuration options might not be enough to pass the exam. Also, and let's put your hand over your heart, memorizing &lt;a href="https://aws.amazon.com/ec2/" rel="noopener noreferrer"&gt;EC2&lt;/a&gt; or &lt;a href="https://aws.amazon.com/s3/" rel="noopener noreferrer"&gt;S3&lt;/a&gt; settings will not make you a better cloud professional.&lt;/p&gt;

&lt;p&gt;Bottom line: get your hands dirty and do some hands-on exercises before sitting the exam. This post will list 16 such exercises if you run out of ideas.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Considerations and prerequisites
&lt;/h2&gt;

&lt;p&gt;Before diving into the exercises, consider the following.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1. AWS Organizations
&lt;/h3&gt;

&lt;p&gt;Create multiple accounts to simulate real-world scenarios. It's free to create AWS accounts, and the exam has &lt;strong&gt;many&lt;/strong&gt; &lt;a href="https://docs.aws.amazon.com/organizations/latest/userguide/orgs_introduction.html" rel="noopener noreferrer"&gt;AWS Organizations&lt;/a&gt;-related questions. If you only have one account, do yourself a favour, set up AWS Organizations (link below), and create at least a &lt;strong&gt;second account&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2. Domain name
&lt;/h3&gt;

&lt;p&gt;Register a domain for DNS-based exercises. Any cheap domain name will do.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3. Cost considerations
&lt;/h3&gt;

&lt;p&gt;Be mindful of potential costs associated with provisioning resources. For example, a &lt;code&gt;t3-micro&lt;/code&gt; EC2 instance is sufficient. The exercises won't require you to run a heavy workload. Also, don't forget to delete any CloudWatch log groups created or set the retention period to a low value, like 3 days.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.4. On-premise environment
&lt;/h3&gt;

&lt;p&gt;You can do your best to cover as many services as possible with hands-on exercises and labs, but you might face some obstacles along the way. If you don't have access to an on-premise environment in your current role, you probably won't be able to test &lt;a href="https://docs.aws.amazon.com/directconnect/latest/UserGuide/Welcome.html" rel="noopener noreferrer"&gt;Direct Connect&lt;/a&gt; or &lt;a href="https://docs.aws.amazon.com/vpn/latest/s2svpn/VPC_VPN.html" rel="noopener noreferrer"&gt;Site-to-Site VPN&lt;/a&gt;. Alternatively, you can try simulating these connections with VPC peering.&lt;/p&gt;

&lt;p&gt;The number of related questions is limited, so you should be OK in the exam if you focus on the core services.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.5. Bulk exercises
&lt;/h3&gt;

&lt;p&gt;Some exercises can be built on previous ones. For example, if you create an &lt;a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/auto-scaling-groups.html" rel="noopener noreferrer"&gt;EC2 Auto Scaling group&lt;/a&gt;, you can use it to add an &lt;a href="https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html" rel="noopener noreferrer"&gt;Application Load Balancer&lt;/a&gt; when an exercise needs that set-up.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Disclaimer
&lt;/h2&gt;

&lt;p&gt;While these exercises cover the majority of the concepts tested in the exam, they are not enough to pass it. You will need a comprehensive study and practice beyond these exercises.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The exercises
&lt;/h2&gt;

&lt;p&gt;Now, onto the theme of this post: Let's see the exercises!&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Launch an EC2 instance in a private subnet. Use Session Manager to connect to the instance. What permissions does the IAM role in the instance profile need? What other VPC resource do you need to provision if the VPC has an Internet Gateway?&lt;/li&gt;
  &lt;li&gt;Create two VPCs, VPC A and VPC B. Set up a VPC peering connection between them. Start an EC2 instance in each VPC. Use Session Manager to connect to the instance in VPC A. Ping the instance deployed to VPC B and observe the configurations (route table, security groups) necessary for the connection to work.&lt;/li&gt;
  &lt;li&gt;Create an isolated subnet (a private subnet with no NAT Gateway). Provision an EC2 instance in the subnet. Try connecting to the instance with Session Manager. What VPC interface endpoints do you need to create?&lt;/li&gt;
  &lt;li&gt;Provision an EC2 instance to a VPC. Configure VPC Flow Logs with CloudWatch Logs destination. How many different levels can you configure the flow logs? Ping the instance and observe the flow logs in CloudWatch.&lt;/li&gt;
  &lt;li&gt;Here's the classic firewall problem! Provision one EC2 instance each in two different subnets, Subnet A and Subnet B. Configure security groups that allow ICMP ping from the instance in Subnet A to the instance in Subnet B. Have VPC Flow Logs configured. Remove the default outbound rule that allows all traffic from all destinations (rule number 100) from the Network ACL attached to subnet B. What do you notice in the flow logs?&lt;/li&gt;
  &lt;li&gt;Configure an Auto Scaling group with 2 EC2 instances. Observe the steps. What settings are available in the launch template? What scaling policies are available?&lt;/li&gt;
  &lt;li&gt;Use AWS Certificate Manager to create a certificate to your custom domain. What steps need to be taken to validate domain ownership?&lt;/li&gt;
  &lt;li&gt;Launch an EC2 Auto Scaling group with at least 2 instances and add an Application Load Balancer to the infrastructure. Configure a CloudFront distribution with the load balancer being the origin. Add a certificate to the distribution. Which region do you need to create the certificate in? Configure a record with simple routing in Route 53. Which record type should you use? Could you use an alias?&lt;/li&gt;
  &lt;li&gt;Create a Lambda function with the default settings. Replace the &lt;code&gt;// TODO implement&lt;/code&gt; line with the following: &lt;code&gt;console.log('Hello world!')&lt;/code&gt;. Deploy and invoke the function. Head over to CloudWatch and create a metric filter in the function's log group. Filter for the word &lt;q&gt;world&lt;/q&gt;. Configure an alarm on this custom metric that sends you an email notification when the word &lt;q&gt;world&lt;/q&gt; occurs in the function logs.&lt;/li&gt;
  &lt;li&gt;Configure failover routing in Route 53. Use two Application Load Balancers with EC2 instance targets in two different regions.&lt;/li&gt;
  &lt;li&gt;Launch two EC2 instances in a VPC. Create a private hosted zone and assign it to the VPC. Ping one instance from the other using a custom private domain name.&lt;/li&gt;
  &lt;li&gt;Configure centralized notification for the Health Dashboard for all your accounts in AWS Organizations. (You might want to do this anyway to prevent Amazon from sending you an email for each active region in each account when, for example, a Lambda runtime becomes end-of-life.)&lt;/li&gt;
  &lt;li&gt;Set up an email notification automation when an EC2 instance is terminated. Hint: You can use EventBridge.&lt;/li&gt;
  &lt;li&gt;Create a tag-based resource group that collects resources with the &lt;code&gt;Project: demo&lt;/code&gt; tag. Add the EC2 instance resource type (optionally, you can add other resource types).&lt;/li&gt;
  &lt;li&gt;Provision 3 EC2 instances and add them the &lt;code&gt;Project: demo&lt;/code&gt; tag. Install an Apache server on all of them at once using tags or a resource group.&lt;/li&gt;
  &lt;li&gt;Create a simple portfolio in Service Catalog in Account A. Share the portfolio with Account B. What can and cannot users of Account B do with the portfolio and the products in it?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  5. Summary
&lt;/h2&gt;

&lt;p&gt;These exercises will help you get an idea about the expectations in the AWS Certified CloudOps Engineer - Associate certification exam.&lt;/p&gt;

&lt;p&gt;I hope you'll find them useful.&lt;/p&gt;

&lt;p&gt;Enjoy!&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Further reading and learning
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/certification/certified-cloudops-engineer-associate/" rel="noopener noreferrer"&gt;AWS Certified CloudOps Engineer - Associate&lt;/a&gt; - Everything official about the exam&lt;/p&gt;

&lt;p&gt;&lt;a href="https://skillbuilder.aws/category/exam-prep/cloudops-engineer-associate-SOA-C03" rel="noopener noreferrer"&gt;&lt;br&gt;
AWS Certified CloudOps Engineer - Associate (SOA-C03)&lt;/a&gt; - Exam preparation plan&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/organizations/latest/userguide/orgs_tutorials_basic.html" rel="noopener noreferrer"&gt;Tutorial: Creating and configuring an organization&lt;/a&gt; - Getting started with AWS Organizations&lt;/p&gt;

</description>
      <category>aws</category>
      <category>certification</category>
      <category>cloudskills</category>
    </item>
    <item>
      <title>Updating data status with API Gateway WebSocket API</title>
      <dc:creator>Arpad Toth</dc:creator>
      <pubDate>Thu, 04 Dec 2025 10:58:04 +0000</pubDate>
      <link>https://dev.to/aws-builders/updating-data-status-with-api-gateway-websocket-api-2al</link>
      <guid>https://dev.to/aws-builders/updating-data-status-with-api-gateway-websocket-api-2al</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: We can use Amazon API Gateway's WebSocket API to keep the front-end up to date with the latest data, providing a near-real-time alternative to polling a REST API endpoint for status notifications.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;



&lt;ul&gt;
&lt;li&gt;1. The scenario&lt;/li&gt;
&lt;li&gt;
2. Solution overview

&lt;ul&gt;
&lt;li&gt;2.1. Requirements&lt;/li&gt;
&lt;li&gt;2.2. Options&lt;/li&gt;
&lt;li&gt;2.3. Diagram&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;3. Pre-requisites&lt;/li&gt;

&lt;li&gt;

4. Main steps

&lt;ul&gt;
&lt;li&gt;4.1. Ingestion&lt;/li&gt;
&lt;li&gt;4.2. Click data persistence&lt;/li&gt;
&lt;li&gt;4.3. Dashboard connection&lt;/li&gt;
&lt;li&gt;4.4. Persisting connection IDs&lt;/li&gt;
&lt;li&gt;4.5. Retrieving data from the table&lt;/li&gt;
&lt;li&gt;4.6. Broadcasting click data&lt;/li&gt;
&lt;li&gt;4.7. Receiving click results&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

5. Considerations

&lt;ul&gt;
&lt;li&gt;5.1. No need for a REST API?&lt;/li&gt;
&lt;li&gt;5.2. Asynchronous workflows&lt;/li&gt;
&lt;li&gt;5.3. Flexibility&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;6. Summary&lt;/li&gt;

&lt;li&gt;

7. Further reading
&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. The scenario
&lt;/h2&gt;

&lt;p&gt;Suppose we operate a click data processing application. Customers interact with links and buttons on our page, and the application records the number and type of clicks.&lt;/p&gt;

&lt;p&gt;Our application also features a &lt;strong&gt;dashboard&lt;/strong&gt; where authorized users monitor current click counts. The dashboard is browser-based, and, as expected, the front-end exclusively displays data with no business logic involved.&lt;/p&gt;

&lt;p&gt;The application persists click results in a database, and the client retrieves them from the backend. But here's a question: How should we display the current click count status?&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Solution overview
&lt;/h2&gt;

&lt;p&gt;First, let's review the options briefly.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1. Requirements
&lt;/h3&gt;

&lt;p&gt;Some of the requirements are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The dashboard should display the clicks real or near real time.&lt;/li&gt;
  &lt;li&gt;Minimize human interaction. The solution should be as automated as possible.&lt;/li&gt;
  &lt;li&gt;Click results should be persisted in a database.&lt;/li&gt;
  &lt;li&gt;The solution should scale.&lt;/li&gt;
  &lt;li&gt;The architecture needs to be extendable and its elements should be transferrable to other business scenarios.&lt;/li&gt;
  &lt;li&gt;The infrastructure must be hosted on AWS and be serverless.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.2. Options
&lt;/h3&gt;

&lt;p&gt;We can basically consider two solutions: polling and websockets.&lt;/p&gt;

&lt;p&gt;After testing the &lt;strong&gt;polling&lt;/strong&gt; solution, we might found it reasonable but expensive. The client must send a request to a &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" rel="noopener noreferrer"&gt;REST API&lt;/a&gt; every few seconds to get the current state of the click results.&lt;/p&gt;

&lt;p&gt;Frequent network calls increase the application's response time. Also, polling logic in the client requires work, like managing the poller’s status and clearing intervals. Still, polling can be useful in some cases.&lt;/p&gt;

&lt;p&gt;Instead, we can implement a &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api.html" rel="noopener noreferrer"&gt;&lt;b&gt;WebSocket API&lt;/b&gt;&lt;/a&gt;, rather than writing poller code that would invoke an endpoint every few seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3. Diagram
&lt;/h3&gt;

&lt;p&gt;The following diagram summarizes the architecture.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7wjr67f8om5q8vdrgek0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7wjr67f8om5q8vdrgek0.png" alt="Click data architecture" width="800" height="627"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This post focuses on keeping the client (the browser application) synchronised with the current click status. The   Considerations section discusses variations to the diagrammed architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pre-requisites
&lt;/h2&gt;

&lt;p&gt;This post is not a comprehensive tutorial, instead, it highlights key steps. It does not explain how to&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;create API Gateways (REST and WebSocket)&lt;/li&gt;
  &lt;li&gt;create Lambda functions&lt;/li&gt;
  &lt;li&gt;create a DynamoDB table&lt;/li&gt;
  &lt;li&gt;enable and work with DynamoDB streams.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Links to relevant documentation will be provided at the end for those who need them.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Main steps
&lt;/h2&gt;

&lt;p&gt;Let's quickly review the main steps in the flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. Ingestion
&lt;/h3&gt;

&lt;p&gt;When users click any elements (links or buttons) on the page, the client sends click data to the &lt;code&gt;/ingest&lt;/code&gt; REST endpoint created in API Gateway. The &lt;code&gt;IngestClickData&lt;/code&gt; &lt;strong&gt;Lambda function&lt;/strong&gt; receives, validates and possibly transforms the data.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2. Click data persistence
&lt;/h3&gt;

&lt;p&gt;We persist click stream data in a &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStartedDynamoDB.html" rel="noopener noreferrer"&gt;DynamoDB table&lt;/a&gt;. The &lt;code&gt;IngestClickData&lt;/code&gt; function persists click data to the table using the &lt;code&gt;PutItem&lt;/code&gt; operation.&lt;/p&gt;

&lt;p&gt;Alternatively, some business cases may justify &lt;strong&gt;direct integration&lt;/strong&gt; between API Gateway and DynamoDB. If the input does not require validation or transformation, we can omit the Lambda function. Choosing this approach saves costs and improves performance by a few dozen (or even hundreds) of milliseconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3. Dashboard connection
&lt;/h3&gt;

&lt;p&gt;On the dashboard, administrators monitor the status of click data.&lt;/p&gt;

&lt;p&gt;As soon as the dashboard page is loaded, the client connects to the &lt;strong&gt;WebSocket&lt;/strong&gt; API. The &lt;code&gt;connectWebSocket&lt;/code&gt; function in the front-end code might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;connectWebSocket&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Use the WebSocket object&lt;/span&gt;
  &lt;span class="nx"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WEBSOCKET_URL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. When the connection is open&lt;/span&gt;
  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onopen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// If we want to display the connection status&lt;/span&gt;
    &lt;span class="c1"&gt;// in the UI:&lt;/span&gt;
    &lt;span class="nf"&gt;updateConnectionStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ConnectionStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CONNECTED&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. When a click data message is received,&lt;/span&gt;
  &lt;span class="c1"&gt;// `handleMessage` is invoked&lt;/span&gt;
  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;handleMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. When the connection is closed&lt;/span&gt;
  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onclose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// If we want to display the connection status&lt;/span&gt;
    &lt;span class="c1"&gt;// in the UI:&lt;/span&gt;
    &lt;span class="nf"&gt;updateConnectionStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ConnectionStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DISCONNECTED&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// We can implement reconnection logic&lt;/span&gt;
    &lt;span class="c1"&gt;// here if it wasn't an intentional close&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// 5. Error occurred&lt;/span&gt;
  &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onerror&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;updateConnectionStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ConnectionStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DISCONNECTED&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// The onerror event is always followed by onclose, so&lt;/span&gt;
    &lt;span class="c1"&gt;// reconnection will be handled in the onclose handler&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code uses the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket" rel="noopener noreferrer"&gt;native WebSocket API&lt;/a&gt; (1). The &lt;code&gt;WEBSOCKET_URL&lt;/code&gt;'s form is similar to REST-type APIs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wss://&amp;lt;API_ID&amp;gt;.execute-api.eu-central-1.amazonaws.com/&amp;lt;STAGE&amp;gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we use infrastructure-as-code tools, like the &lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/home.html" rel="noopener noreferrer"&gt;CDK&lt;/a&gt;, we can inject the WebSocket URL into the client code dynamically.&lt;/p&gt;

&lt;p&gt;Then, we listen to the available WebSocket events. The &lt;code&gt;open&lt;/code&gt; event (2) is fired when a connection to the API is established. If we implement any reconnection logic (possibly with exponential backoff), we can clear any pending timeout here. We can also update the connection status here if we want to display it in the UI.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;message&lt;/code&gt; event (3) indicates a new click status from the backend (see below). The &lt;code&gt;handleMessage&lt;/code&gt; function performs the logic to update a counter or display the current count in a chart.&lt;/p&gt;

&lt;p&gt;For closed connections (4) and errors (5), we can implement reconnection logic when reasonable, for example, when the connection closure was unintentional.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4. Persisting connection IDs
&lt;/h3&gt;

&lt;p&gt;When we create a WebSocket API in API Gateway, three pre-defined routes are available: &lt;code&gt;$connect&lt;/code&gt;, &lt;code&gt;$disconnect&lt;/code&gt; and &lt;code&gt;$default&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$connect&lt;/code&gt; triggers when the client (the dashboard page) opens a new WebSocket connection to the API. As with the REST-type APIs, we can assign a &lt;strong&gt;Lambda integration&lt;/strong&gt; to the route, specifically the &lt;code&gt;PersistConnectionIds&lt;/code&gt; function. As its name suggests, this function stores the WebSocket connection IDs (issued by API Gateway) in the DynamoDB table. The stream processor function will need the connection IDs to broadcast click data statuses to the connected clients (see below).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;$disconnect&lt;/code&gt; route is wired to the &lt;code&gt;DeleteConnectionIDs&lt;/code&gt; function, which, however unbelievable it may sound, removes the corresponding connection ID from the table. This Lambda function runs when an admin user leaves the dashboard page or closes the browser tab. With the &lt;code&gt;DeleteConnectionIDs&lt;/code&gt; function removing the unused connection IDs, the stream processor will not broadcast click data results to unconnected dashboard clients.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;$default&lt;/code&gt; serves as a fallback if no routes match. This route is not implemented here.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.5. Retrieving data from the table
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;IngestClickData&lt;/code&gt; saves a new piece of data to the DynamoDB table, a &lt;strong&gt;DynamoDB Streams&lt;/strong&gt; event is triggered. DynamoDB Streams capture data modification events, and here, whenever data in the table changes, related services can respond in near real time.&lt;/p&gt;

&lt;p&gt;Although the diagram uses only one table, another approach could be to split the click data and connection IDs into separate tables.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;StreamProcessor&lt;/code&gt; function only cares about changes to click data statuses. With a single table storing both click data and connection IDs, we might want to implement an &lt;strong&gt;event filter&lt;/strong&gt; here to optimize cost. The function doesn't need to run when &lt;code&gt;PersistConnectionIDs&lt;/code&gt; saves the connection IDs to the table.&lt;/p&gt;

&lt;p&gt;We can easily write a &lt;strong&gt;filter&lt;/strong&gt; in the CDK infrastructure code that monitors a &lt;strong&gt;dedicated attribute&lt;/strong&gt; of the item. Assuming that click data items have an &lt;code&gt;entityType: CLICK&lt;/code&gt; attribute, we can write a filter similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CDK code!!&lt;/span&gt;
&lt;span class="c1"&gt;// `streamProcessorLambda` is a Lambda Function&lt;/span&gt;
&lt;span class="c1"&gt;// construct defined earlier&lt;/span&gt;
&lt;span class="nx"&gt;streamProcessorLambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambdaEventSources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DynamoEventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clickstreamTable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other properties&lt;/span&gt;
    &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FilterCriteria&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;NewImage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;entityType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;S&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FilterRule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CLICK&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the processor function runs only when click data is persisted, not when a connection ID is stored.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;StreamProcessor&lt;/code&gt; function's handler can be similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// stream-processor.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DynamoDBStreamEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Process each stream record&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Records&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// 1. Extract click data from NEW_IMAGE&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clickData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractClickData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// 2. Broadcast to all active connections&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;broadcastClickEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clickData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// handle errors here&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, the handler extracts the data from the stream event record (1) containing the current status. I don't show this code for brevity, but I have attached some links below that describe how to do it.&lt;/p&gt;

&lt;p&gt;Then, &lt;code&gt;StreamProcessor&lt;/code&gt;'s &lt;code&gt;broadcastClickEvent&lt;/code&gt; function will &lt;strong&gt;query&lt;/strong&gt; all connection IDs from the table. Then, it &lt;strong&gt;iterates&lt;/strong&gt; over the array of connection IDs, and invokes the &lt;code&gt;broadcastToConnection&lt;/code&gt; utility function for &lt;strong&gt;each&lt;/strong&gt; connection ID (2).&lt;/p&gt;

&lt;h3&gt;
  
  
  4.6. Broadcasting click data
&lt;/h3&gt;

&lt;p&gt;An extract of the &lt;code&gt;broadcastToConnection&lt;/code&gt; function may look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ApiGatewayManagementApiClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;PostToConnectionCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws-sdk/client-apigatewaymanagementapi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// more import statements&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiGatewayClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ApiGatewayManagementApiClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WEBSOCKET_API_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// more code&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;broadcastToConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;connectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WebSocketMessage&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messageData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;apiGatewayClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PostToConnectionCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;ConnectionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;connectionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messageData&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// handle errors here&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function receives the &lt;code&gt;message&lt;/code&gt; (i.e., the click data status) and a &lt;strong&gt;single connection ID&lt;/strong&gt;. The parent function, &lt;code&gt;broadcastClickEvent&lt;/code&gt; (see above) invokes this function for &lt;strong&gt;each connection ID&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We create an &lt;code&gt;ApiGatewayManagementClient&lt;/code&gt; instance with the WebSocket endpoint, and use the &lt;code&gt;PostToConnection&lt;/code&gt; action to send the stringified &lt;code&gt;message&lt;/code&gt; to the client connected to the WebSocket API with the &lt;code&gt;connectionId&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.7. Receiving click results
&lt;/h3&gt;

&lt;p&gt;Finally, the dashboard client receives the click data that &lt;code&gt;StreamProcessor&lt;/code&gt; sends to the WebSocket API. When this happens, the &lt;code&gt;message&lt;/code&gt; event available via the &lt;code&gt;onmessage&lt;/code&gt; property is emitted. As discussed above, the &lt;code&gt;handleMessage&lt;/code&gt; front-end function manages the data updates in the UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Considerations
&lt;/h2&gt;

&lt;p&gt;The solution described here updates the click data status in near real-time. There's no expensive polling involved in the architecture. The browser application will automatically receive the new data through the WebSocket API.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1. No need for a REST API?
&lt;/h3&gt;

&lt;p&gt;If there's no requirement for REST APIs, we can simplify the architecture by sending click data to the WebSocket API. We can create a &lt;strong&gt;custom route&lt;/strong&gt; (or use the &lt;code&gt;$default&lt;/code&gt; route) and integrate it with the &lt;code&gt;IngestClickData&lt;/code&gt; function directly.&lt;/p&gt;

&lt;p&gt;If, for example, we want to integrate external systems with a webhook or work with clients that depend on REST endpoints, the solution presented here can be viable.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2. Asynchronous workflows
&lt;/h3&gt;

&lt;p&gt;The architecture works particularly well for asynchronous workflows, like &lt;strong&gt;order processing&lt;/strong&gt; or &lt;strong&gt;booking&lt;/strong&gt;. Typically, we don't want users to wait for every step of the workflow to finish. It would result in a suboptimal user experience. Instead, we return an &lt;code&gt;Accepted&lt;/code&gt; response after the user has placed the order.&lt;/p&gt;

&lt;p&gt;In this case, we probably want to &lt;a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/integrate-amazon-api-gateway-with-amazon-sqs-to-handle-asynchronous-rest-apis.html" rel="noopener noreferrer"&gt;connect&lt;/a&gt; the REST-type API Gateway to an &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html" rel="noopener noreferrer"&gt;SQS queue&lt;/a&gt;. A Lambda function can poll the queue for messages and process them at a pace that doesn't overload the backend.&lt;/p&gt;

&lt;p&gt;The front-end application can then connect to a WebSocket API and receive the updated order status from the server as described above.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3. Flexibility
&lt;/h3&gt;

&lt;p&gt;The architecture is flexible and extendable. In case of steadily heavy volume, API Gateway can &lt;strong&gt;directly&lt;/strong&gt; integrate with &lt;a href="https://docs.aws.amazon.com/streams/latest/dev/introduction.html" rel="noopener noreferrer"&gt;Kinesis Data Streams&lt;/a&gt;, as described in the &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/integrating-api-with-aws-services-kinesis.html" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;. As with SQS queues, Lambda functions can poll the stream and process the records at their own pace.&lt;/p&gt;

&lt;p&gt;As always, the solution presented here is &lt;strong&gt;not&lt;/strong&gt; production ready, and cannot be taken as is. You always need to test your solutions before deploying them to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Summary
&lt;/h2&gt;

&lt;p&gt;WebSocket APIs can be a good choice when we don't want to frequently poll a REST API for status data. API Gateway supports WebSocket APIs.&lt;/p&gt;

&lt;p&gt;A key element in WebSocket API architectures is to persist connection IDs in a database, so that the resource (e.g., a Lambda function) broadcasting data to the API is aware of the active connections. Connected clients then automatically receive the current status through the WebSocket API.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Further reading
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/getting-started.html" rel="noopener noreferrer"&gt;Getting started with Lambda&lt;/a&gt; - How to create a Lambda function&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-create-api.html" rel="noopener noreferrer"&gt;Creating a REST API in Amazon API Gateway&lt;/a&gt; - The title says it all&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStartedDynamoDB.html" rel="noopener noreferrer"&gt;Getting started with DynamoDB&lt;/a&gt; - DynamoDB basics&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html" rel="noopener noreferrer"&gt;Change data capture for DynamoDB Streams&lt;/a&gt; - AWS documentation page for DynamoDB Streams&lt;/p&gt;

&lt;p&gt;&lt;a href="https://arpadt.com/articles/dynamodb-stream-fanout" rel="noopener noreferrer"&gt;Triggering multiple workflows with DynamoDB Streams&lt;/a&gt; - A more subjective and definitely more biased article on DynamoDB Streams&lt;/p&gt;

&lt;p&gt;&lt;a href="https://arpadt.com/articles/event-based-flow-dynamodb-streams" rel="noopener noreferrer"&gt;Building event-driven workflows with DynamoDB Streams&lt;/a&gt; - Another example of my emotionless self-promotion&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html" rel="noopener noreferrer"&gt;Change data capture for DynamoDB Streams&lt;/a&gt; - AWS documentation page for DynamoDB Streams&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/triggering-multiple-workflows-with-dynamodb-streams-38h1" rel="noopener noreferrer"&gt;Triggering multiple workflows with DynamoDB Streams&lt;/a&gt; - A more subjective and definitely more biased article on DynamoDB Streams&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/aws-builders/building-event-driven-workflows-with-dynamodb-streams-3a36" rel="noopener noreferrer"&gt;Building event-driven workflows with DynamoDB Streams&lt;/a&gt; - Another example of my emotionless self-promotion&lt;/p&gt;

</description>
      <category>aws</category>
      <category>apigateway</category>
      <category>lambda</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Choosing between EventBridge, SNS, and SQS for event-driven patterns</title>
      <dc:creator>Arpad Toth</dc:creator>
      <pubDate>Mon, 02 Jun 2025 08:29:50 +0000</pubDate>
      <link>https://dev.to/aws-builders/choosing-between-eventbridge-sns-and-sqs-for-event-driven-patterns-1l4g</link>
      <guid>https://dev.to/aws-builders/choosing-between-eventbridge-sns-and-sqs-for-event-driven-patterns-1l4g</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: AWS offers multiple services for decoupling business domains in event-driven patterns. The three main ones are EventBridge, SNS, and SQS. Use EventBridge for targeted content-based routing when you need to match complex rules. When multiple services or users need real-time notifications, SNS is a reasonable choice. Use SQS to buffer events when you want to protect downstream resources.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;



&lt;ul&gt;
&lt;li&gt;1. The scenario&lt;/li&gt;
&lt;li&gt;
2. EventBridge

&lt;ul&gt;
&lt;li&gt;2.1. Two short paragraphs on EventBridge&lt;/li&gt;
&lt;li&gt;2.2. EventBridge rule&lt;/li&gt;
&lt;li&gt;2.3. EventBridge use cases&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

3. SNS

&lt;ul&gt;
&lt;li&gt;3.1. About SNS - you can skip this if you're a pro&lt;/li&gt;
&lt;li&gt;3.2. Fanout&lt;/li&gt;
&lt;li&gt;3.3. SNS use cases&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

4. SQS

&lt;ul&gt;
&lt;li&gt;4.1. SQS basics&lt;/li&gt;
&lt;li&gt;4.2. FIFO queues for order&lt;/li&gt;
&lt;li&gt;4.3. When to use SQS&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

5. Considerations

&lt;ul&gt;
&lt;li&gt;5.1. SNS message filtering&lt;/li&gt;
&lt;li&gt;5.2. EventBridge Pipes&lt;/li&gt;
&lt;li&gt;5.3. Cost&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;6. Summary&lt;/li&gt;

&lt;li&gt;7. Wrap up&lt;/li&gt;

&lt;li&gt;

8. Further reading
&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Well, not everything in life is a win. Unfortunately, dev.to hasn't let me upload the article images for days, so, although there are references to them, no diagrams can be found in this post. Please read the article &lt;a href="https://arpadt.com/articles/eb-sns-sqs" rel="noopener noreferrer"&gt;here&lt;/a&gt; if you want to view them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. The scenario
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is.html" rel="noopener noreferrer"&gt;EventBridge&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/sns/latest/dg/welcome.html" rel="noopener noreferrer"&gt;SNS&lt;/a&gt;, and &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html" rel="noopener noreferrer"&gt;SQS&lt;/a&gt; can all be used to decouple services and provide the foundation for an event-driven architecture.&lt;/p&gt;

&lt;p&gt;Our scenario involves Alice's &lt;strong&gt;e-commerce&lt;/strong&gt; application, which consists of several microservices. This article focuses on a small portion of the system, specifically &lt;strong&gt;order processing&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I’ll explore three specific tasks or challenges from the order processing business requirements and discuss which of the three AWS services is the most suitable choice for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. EventBridge
&lt;/h2&gt;

&lt;p&gt;Users can place three types of orders in this application: &lt;strong&gt;standard&lt;/strong&gt;, &lt;strong&gt;express&lt;/strong&gt;, and &lt;strong&gt;international&lt;/strong&gt;. Alice decided to create different workflows for each order type, as they require distinct handling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Express&lt;/strong&gt; orders need immediate processing. &lt;strong&gt;Standard&lt;/strong&gt; orders are less urgent and can be processed in batches at the end of the day. &lt;strong&gt;International&lt;/strong&gt; orders require manual intervention.&lt;/p&gt;

&lt;p&gt;Alice’s challenge is to &lt;strong&gt;route&lt;/strong&gt; orders to their corresponding workflows based on the &lt;code&gt;type&lt;/code&gt; property of the &lt;code&gt;order&lt;/code&gt; event object.&lt;/p&gt;

&lt;p&gt;A simplified version of the &lt;code&gt;order&lt;/code&gt; event might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"express"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"12345"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"itemId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"itemName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Fitness bar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"unitPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;129.99&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;129.99&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c1"&gt;// many more properties&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EventBridge excels in this scenario!&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1. Two short paragraphs on EventBridge
&lt;/h3&gt;

&lt;p&gt;EventBridge is a &lt;strong&gt;serverless event router&lt;/strong&gt; that performs &lt;strong&gt;content-based&lt;/strong&gt; routing to assigned &lt;strong&gt;targets&lt;/strong&gt; (up to five). We create &lt;strong&gt;rules&lt;/strong&gt; that define the desired &lt;strong&gt;event pattern&lt;/strong&gt; for each target. When a matching event arrives at the &lt;strong&gt;event bus&lt;/strong&gt; (default or custom), EventBridge invokes the target with the event.&lt;/p&gt;

&lt;p&gt;AWS services send events to the &lt;strong&gt;default&lt;/strong&gt; event bus. You can create &lt;strong&gt;custom event buses&lt;/strong&gt; for custom application events (non-AWS service events). While you can use the default event bus for custom events, I prefer separating the two, keeping the default event bus for AWS events only.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2. EventBridge rule
&lt;/h3&gt;

&lt;p&gt;Consider the &lt;strong&gt;express&lt;/strong&gt; order flow. We want to match every &lt;code&gt;order&lt;/code&gt; event where the order originates from the &lt;code&gt;order-placement-service&lt;/code&gt;, is newly &lt;code&gt;placed&lt;/code&gt;, and has a &lt;code&gt;type&lt;/code&gt; of &lt;code&gt;express&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The event pattern for this looks as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"order-placement-service"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail-type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"order.placed"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"express"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;detail-type&lt;/code&gt;, and &lt;code&gt;detail&lt;/code&gt; fields enable the creation of &lt;strong&gt;complex rules&lt;/strong&gt; for specific use cases.&lt;/p&gt;

&lt;p&gt;In Alice’s application, the &lt;code&gt;source&lt;/code&gt; property represents service names, while &lt;code&gt;detail-type&lt;/code&gt; indicates the order status. The &lt;code&gt;detail&lt;/code&gt; field specifies the required property values in the order event object that must be matched.&lt;/p&gt;

&lt;p&gt;When the event bus receives a matching event, EventBridge routes it to the assigned target, in this case, a &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" rel="noopener noreferrer"&gt;Lambda function&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3. EventBridge use cases
&lt;/h3&gt;

&lt;p&gt;EventBridge is a great choice for decoupling services when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You need to match complex event patterns.&lt;/li&gt;
  &lt;li&gt;You want to react to AWS service and support third-party events in real time.&lt;/li&gt;
  &lt;li&gt;You’re designing multi-region or multi-account event-driven architecture.&lt;/li&gt;
  &lt;li&gt;You need to run scheduled jobs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. SNS
&lt;/h2&gt;

&lt;p&gt;Alice’s next event-driven challenge is to &lt;strong&gt;notify&lt;/strong&gt; multiple services (e.g., user notification service, order processing service) when a new order is placed.&lt;/p&gt;

&lt;p&gt;The diagram shows only three services for simplicity, but in reality, many more services (e.g., warehouse, delivery, or marketing) may be interested in the &lt;code&gt;order.placed&lt;/code&gt; &lt;strong&gt;event&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Alice chooses &lt;strong&gt;SNS&lt;/strong&gt; to meet these business requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1. About SNS - you can skip this if you’re a pro
&lt;/h3&gt;

&lt;p&gt;Simple Notification Service (SNS), as its name suggests, focuses on &lt;strong&gt;notifications&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;publisher&lt;/strong&gt; (here, the order placement service) publishes a &lt;strong&gt;message&lt;/strong&gt; to an SNS &lt;strong&gt;topic&lt;/strong&gt;. The topic receives messages of the same type. Services interested in these messages can &lt;strong&gt;subscribe&lt;/strong&gt; to the topic. Whenever a publisher sends a message to the topic, SNS &lt;strong&gt;broadcasts&lt;/strong&gt; it to all subscribers.&lt;/p&gt;

&lt;p&gt;SNS supports multiple subscriber &lt;strong&gt;protocols&lt;/strong&gt;: email, text messages, HTTP, SQS, Lambda, and more. This allows &lt;strong&gt;direct notifications&lt;/strong&gt; to users’ phones or email addresses, though I would prefer a dedicated notification service in this case.&lt;/p&gt;

&lt;p&gt;If message order is critical, you can use a &lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sns-fifo-topics.html" rel="noopener noreferrer"&gt;FIFO (first-in-first-out) topic&lt;/a&gt;. FIFO topics guarantee message order and exactly-once delivery, but they have lower throughput compared to standard topics.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2. Fanout
&lt;/h3&gt;

&lt;p&gt;The diagram illustrates a scenario where a publisher sends a message to the topic, and SNS delivers it to multiple subscribers, including SMS, SQS, and Lambda protocols.&lt;/p&gt;

&lt;p&gt;This is called the &lt;strong&gt;fanout&lt;/strong&gt; pattern, a key tool in event-driven design. Standard topics support millions of subscribers, enabling wide fanouts with SNS.&lt;/p&gt;

&lt;p&gt;In Alice’s application, the EventBridge target Lambda function publishes the event to the express workflow topic, and SNS notifies relevant services in real time.&lt;/p&gt;

&lt;p&gt;The Lambda function requires the &lt;code&gt;sns:Publish&lt;/code&gt; permission in its execution role.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3. SNS use cases
&lt;/h3&gt;

&lt;p&gt;Use SNS when:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;You need to notify a large number of subscribers in real time.&lt;/li&gt;
  &lt;li&gt;You’re creating alarms, for example, in CloudWatch.&lt;/li&gt;
  &lt;li&gt;You’re designing a pub/sub (publisher/subscriber) pattern with known targets (subscribers).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. SQS
&lt;/h2&gt;

&lt;p&gt;Alice’s final challenge is protecting a &lt;strong&gt;third-party API&lt;/strong&gt; endpoint called by one of the services. Like many third-party integrations, the endpoint is rate-limited, say, to one request per second.&lt;/p&gt;

&lt;p&gt;Issues arise when more orders flood the system than the API can handle. Alice wants to minimize throttling errors from the external service.&lt;/p&gt;

&lt;p&gt;She needs a &lt;strong&gt;temporary message store&lt;/strong&gt; to buffer messages, allowing the consumer compute resource to process them at a pace suitable for the downstream API.&lt;/p&gt;

&lt;p&gt;One of AWS’s earliest services, &lt;strong&gt;SQS&lt;/strong&gt;, serves this purpose.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. SQS basics
&lt;/h3&gt;

&lt;p&gt;Simple Queue Service (SQS) is a serverless queue service that decouples services by &lt;strong&gt;buffering&lt;/strong&gt; messages. Messages can be stored in queues for up to 14 days.&lt;/p&gt;

&lt;p&gt;Unlike SNS, SQS is a &lt;strong&gt;point-to-point&lt;/strong&gt; service.&lt;/p&gt;

&lt;p&gt;Typically, there is &lt;strong&gt;one producer&lt;/strong&gt; and &lt;strong&gt;one consumer&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;While multiple producers can technically send to the same queue, I recommend creating separate queues for each producer to maintain clear responsibilities. Multiple producers for a single queue can complicate debugging and tracing. As a serverless service, SQS is billed by the number of &lt;strong&gt;requests&lt;/strong&gt;, and you pay nothing when it’s unused. SQS also offers a generous free tier, so there’s little reason not to create multiple queues in these scenarios.&lt;/p&gt;

&lt;p&gt;Similarly, multiple consumers can be assigned to a queue, but they will compete for messages, meaning not every consumer processes every message. This can cause issues if the consumers implement different business logic.&lt;/p&gt;

&lt;p&gt;The consumer must &lt;strong&gt;delete&lt;/strong&gt; a message from the queue after successful processing; otherwise, it becomes visible again for reprocessing.&lt;/p&gt;

&lt;p&gt;SQS has a default retry policy, delivering unsuccessfully processed messages to a &lt;strong&gt;dead-letter queue&lt;/strong&gt; (DLQ) after the maximum retry attempts. Teams can configure alarms on queue length and investigate issues when messages reach the DLQ.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2. FIFO queues for order
&lt;/h3&gt;

&lt;p&gt;SQS queues come in two types: &lt;strong&gt;standard&lt;/strong&gt; and &lt;strong&gt;FIFO&lt;/strong&gt;, with principles similar to SNS FIFO topics.&lt;/p&gt;

&lt;p&gt;Standard queues offer higher throughput but don’t guarantee message order and provide at-least-once delivery, requiring consumers to implement &lt;strong&gt;idempotency&lt;/strong&gt; mechanisms.&lt;/p&gt;

&lt;p&gt;FIFO queues ensure that messages enter and leave in the same order and guarantee exactly-once delivery.&lt;/p&gt;

&lt;p&gt;Alice’s application uses an SQS queue to protect the third-party (e.g., delivery partner) endpoint in the scenario described.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3. When to use SQS
&lt;/h3&gt;

&lt;p&gt;SQS is ideal for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Temporarily storing messages to protect downstream resources.&lt;/li&gt;
  &lt;li&gt;Implementing reliable message delivery and processing with retries.&lt;/li&gt;
  &lt;li&gt;Isolating unprocessed messages for debugging.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SQS pairs well with &lt;strong&gt;Lambda functions&lt;/strong&gt;. Lambda &lt;strong&gt;polls&lt;/strong&gt; the queue for messages (polling invocation model) and automatically deletes processed messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Considerations
&lt;/h2&gt;

&lt;p&gt;Let’s discuss some additional considerations.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1. SNS message filtering
&lt;/h3&gt;

&lt;p&gt;SNS supports &lt;strong&gt;message filtering&lt;/strong&gt;, allowing subscribers to receive only messages matching specific attributes. By attaching a &lt;strong&gt;filter policy&lt;/strong&gt; to a subscription, subscribers receive a subset of messages. Without a filter policy, subscribers receive all messages published to the topic.&lt;/p&gt;

&lt;p&gt;I have seen using SNS and EventBridge interchangeably, especially with the SNS message filtering feature. It's not easy to decide which one to use in some cases. I prefer EventBridge when the problem is more routing-focused, like the one with the different order types. I choose SNS when I want the message to be broadcast and received by other interested entities.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2. EventBridge Pipes
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-pipes.html" rel="noopener noreferrer"&gt;EventBridge Pipes&lt;/a&gt; combines aspects of EventBridge and SQS. It reacts to AWS service events in a point-to-point manner and offers filtering and message enrichment capabilities. It can provide a good alternative solution for these services in some cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3. Cost
&lt;/h3&gt;

&lt;p&gt;Since EventBridge, SNS, and SQS serve different use cases — while all help decouple (micro)services — I don’t find it relevant to compare their costs.&lt;/p&gt;

&lt;p&gt;Cost management for each service could be the topic of a separate article (or three, one for each), so I won’t discuss it here.&lt;/p&gt;

&lt;p&gt;I’ve linked the corresponding pricing pages at the bottom of the post. You can also use the &lt;a href="https://calculator.aws/#/" rel="noopener noreferrer"&gt;Pricing Calculator&lt;/a&gt; to estimate costs for your workload if needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Summary
&lt;/h2&gt;

&lt;p&gt;The following table summarizes the key differences between EventBridge, SNS, and SQS.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;em&gt;Feature&lt;/em&gt;&lt;/th&gt;
&lt;th&gt;&lt;em&gt;EventBridge&lt;/em&gt;&lt;/th&gt;
&lt;th&gt;&lt;em&gt;SNS&lt;/em&gt;&lt;/th&gt;
&lt;th&gt;&lt;em&gt;SQS&lt;/em&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Content-based event routing&lt;/td&gt;
&lt;td&gt;Real-time notifications&lt;/td&gt;
&lt;td&gt;Message buffering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Communication pattern&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Many-to-many with filtering&lt;/td&gt;
&lt;td&gt;One-to-many (fanout)&lt;/td&gt;
&lt;td&gt;Point-to-point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Use cases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Complex event pattern matching, AWS service events, scheduled jobs&lt;/td&gt;
&lt;td&gt;Notifying multiple subscribers in real time, alarms, pub/sub pattern&lt;/td&gt;
&lt;td&gt;Protecting downstream resources, reliable message delivery with retries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Delivery guarantee&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;At-least-once&lt;/td&gt;
&lt;td&gt;At-least-once (standard), exactly-once (FIFO)&lt;/td&gt;
&lt;td&gt;At-least-once (standard), exactly-once (FIFO)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Message retention&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No retention (unless archived)&lt;/td&gt;
&lt;td&gt;No retention&lt;/td&gt;
&lt;td&gt;Up to 14 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Integration with Lambda&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Asynchronous invocation model&lt;/td&gt;
&lt;td&gt;Asynchronous invocation model&lt;/td&gt;
&lt;td&gt;Polling invocation model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Special capabilities&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Event pattern matching, cross-account/region routing&lt;/td&gt;
&lt;td&gt;Message filtering with subscription policies&lt;/td&gt;
&lt;td&gt;Message buffering, throttling protection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Wrap up
&lt;/h2&gt;

&lt;p&gt;EventBridge, SNS, and SQS are core AWS services that enable architects and developers to design and implement event-driven architectures.&lt;/p&gt;

&lt;p&gt;This post discussed when to use each service and provided examples for each use case in a fictional application.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Further reading
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-what-is-how-it-works-concepts.html" rel="noopener noreferrer"&gt;Event bus concepts in Amazon EventBridge&lt;/a&gt; - Everything about event buses&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-targets.html" rel="noopener noreferrer"&gt;Event bus targets in Amazon EventBridge&lt;/a&gt; - Targets supported by EventBridge&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule.html" rel="noopener noreferrer"&gt;Creating rules that react to events in Amazon EventBridge&lt;/a&gt; - How to create EventBridge rules&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sns-create-subscribe-endpoint-to-topic.html" rel="noopener noreferrer"&gt;Creating a subscription to an Amazon SNS topic&lt;/a&gt; - Straightforward guidance from the documentation&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sns-message-filtering.html" rel="noopener noreferrer"&gt;Amazon SNS message filtering&lt;/a&gt; - More details on message filtering&lt;/p&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/eventbridge/pricing/" rel="noopener noreferrer"&gt;Amazon EventBridge pricing&lt;/a&gt; - Pricing details for EventBridge&lt;/p&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/sns/pricing/" rel="noopener noreferrer"&gt;Amazon SNS pricing&lt;/a&gt; - Pricing details for SNS&lt;/p&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/sqs/pricing/" rel="noopener noreferrer"&gt;Amazon SQS pricing&lt;/a&gt; - Pricing details for SQS&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>Building MongoDB-based event-driven applications with DocumentDB</title>
      <dc:creator>Arpad Toth</dc:creator>
      <pubDate>Thu, 22 May 2025 10:06:36 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-mongodb-based-event-driven-applications-with-documentdb-pne</link>
      <guid>https://dev.to/aws-builders/building-mongodb-based-event-driven-applications-with-documentdb-pne</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; We can create event-driven architectures that react to changes in MongoDB. Change streams are supported in the latest DocumentDB engine, and we can trigger a Lambda function when a change occurs in the database. Although this article discusses how to send new MongoDB documents to OpenSearch, this is not the only use case. We can publish a message to SNS or EventBridge and inform other services of the database changes.&lt;/p&gt;
&lt;/blockquote&gt;



&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;





&lt;ul&gt;
&lt;li&gt;1. MongoDB vs DynamoDB&lt;/li&gt;
&lt;li&gt;2. The scenario&lt;/li&gt;
&lt;li&gt;3. MongoDB change streams&lt;/li&gt;
&lt;li&gt;
4. Architecture

&lt;ul&gt;
&lt;li&gt;4.1. Overview&lt;/li&gt;
&lt;li&gt;4.2. Detailed view&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

5. Considerations

&lt;ul&gt;
&lt;li&gt;5.1. Not production-ready&lt;/li&gt;
&lt;li&gt;5.2. Testing the app&lt;/li&gt;
&lt;li&gt;5.3. Connecting to the database cluster&lt;/li&gt;
&lt;li&gt;5.4. Direct integration&lt;/li&gt;
&lt;li&gt;5.5. Other event-driven options&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;6. Summary&lt;/li&gt;

&lt;li&gt;

7. Further reading
&lt;/li&gt;

&lt;/ul&gt;



&lt;h2&gt;
  
  
  1. MongoDB vs DynamoDB
&lt;/h2&gt;



&lt;p&gt;I like &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html" rel="noopener noreferrer"&gt;DynamoDB&lt;/a&gt; and use it where it makes sense. I like the fast speed, the elasticity and the fact that it often has a negligible cost compared to other database options.&lt;/p&gt;

&lt;p&gt;But sometimes we have to consider other NoSQL options, like &lt;a href="https://www.mongodb.com/" rel="noopener noreferrer"&gt;MongoDB&lt;/a&gt;. Why do we want to choose MongoDB over DynamoDB? Here are some reasons:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The application needs to store items as JSON documents.&lt;/li&gt;
  &lt;li&gt;Documents are deeply nested, and it would be a lot of effort to transform them into simple attributes that DynamoDB expects.&lt;/li&gt;
  &lt;li&gt;MongoDB has a simple JavaScript syntax. DynamoDB has a steeper learning curve.&lt;/li&gt;
  &lt;li&gt;Your team lead or line manager is afraid of anything new, and they want you to use the good old MongoDB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There might be other reasons (feel free to write them in a comment), but these ones come to mind now.&lt;/p&gt;



&lt;h2&gt;
  
  
  2. The scenario
&lt;/h2&gt;



&lt;p&gt;So, MongoDB is the chosen database. Do we have to abandon the event-driven patterns we can implement with DynamoDB Streams?&lt;/p&gt;

&lt;p&gt;The good news is no, we don't.&lt;/p&gt;

&lt;p&gt;MongoDB supports &lt;strong&gt;change streams&lt;/strong&gt;, which applications can use to react to real-time data changes.&lt;/p&gt;

&lt;p&gt;Luckily, AWS has a MongoDB-compatible NoSQL database called &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/how-it-works.html" rel="noopener noreferrer"&gt;DocumentDB&lt;/a&gt;, and the latest engine version (&lt;code&gt;5.0.0&lt;/code&gt;) now supports change streams.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. MongoDB change streams
&lt;/h2&gt;

&lt;p&gt;Change streams store event objects for &lt;strong&gt;3 hours&lt;/strong&gt; by default, but we can extend the duration up to 7 days. We must &lt;strong&gt;explicitly&lt;/strong&gt; &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/change_streams.html#change_streams-enabling" rel="noopener noreferrer"&gt;enable change streams&lt;/a&gt; on a &lt;strong&gt;database&lt;/strong&gt; or specific &lt;strong&gt;collections&lt;/strong&gt; in the database.&lt;/p&gt;

&lt;p&gt;A good spot for the change stream code is where we write (and probably cache) the MongoDB client code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create the connection and get the client&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;connectToDatabase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Enable change streams on the collection&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adminDb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;adminDb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;modifyChangeStreams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DB_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;COLLECTION_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;enable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Part of the &lt;code&gt;connectToDatabase()&lt;/code&gt; function may be similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secretsManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SecretsManagerClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;connectToDatabase&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MongoClient&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Get credentials from Secrets Manager&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;secretCommandInput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GetSecretValueCommandInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;SecretId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DOCDB_SECRET_ARN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secretData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;secretsManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GetSecretValueCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretCommandInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;secretData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SecretString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Missing database credentials in config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secretData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SecretString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encodedUsername&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encodedPassword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DOCDB_PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;27017&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;database&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DOCDB_DATABASE&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`mongodb://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;encodedUsername&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;encodedPassword&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  @&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;DOCDB_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?tls=true&amp;amp;replicaSet=rs0
  &amp;amp;readPreference=secondaryPreferred&amp;amp;retryWrites=false`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MongoClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;tlsCAFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PATH_TO_ROOT_CERTIFICATE_PEM_FILE&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;tls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;cachedClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, it's a standard MongoDB connection function.&lt;/p&gt;

&lt;p&gt;I want to highlight one thing in the code. DocumentDB integrates with &lt;a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html" rel="noopener noreferrer"&gt;Secrets Manager&lt;/a&gt;, where database secrets (username and password) are stored. The function must fetch these credentials before it can create the connection URI.&lt;/p&gt;

&lt;p&gt;Now that we have enabled the change streams, let's look at the architecture. How can we use the change stream feature in our applications?&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Architecture
&lt;/h2&gt;

&lt;p&gt;This article will present the simple design of an application where admin users can add &lt;strong&gt;products&lt;/strong&gt;, and users can &lt;strong&gt;search&lt;/strong&gt; for them.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. Overview
&lt;/h3&gt;

&lt;p&gt;The connection point to the application is an &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html" rel="noopener noreferrer"&gt;API Gateway&lt;/a&gt;, which has two endpoints: &lt;code&gt;POST /product&lt;/code&gt; and &lt;code&gt;GET /search&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcjk0oqm77bknyxbe7vxa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcjk0oqm77bknyxbe7vxa.png" alt="From DocumentDB to OpenSearch" width="800" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Administrators upload the new product info by sending the &lt;code&gt;product&lt;/code&gt; object in the request body to the &lt;code&gt;/product&lt;/code&gt; endpoint. In this sample project, part of the &lt;code&gt;product&lt;/code&gt; schema looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"productId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"productName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wireless Headphones"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Noise-cancelling wireless headphones"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"brand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SoundPro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Electronics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;199.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ratings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"averageRating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.7&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c1"&gt;// other product-related properties, many of which are nested&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" rel="noopener noreferrer"&gt;Lambda function&lt;/a&gt; will act as the endpoint integration and save new products to the database.&lt;/p&gt;

&lt;p&gt;As discussed above, I chose &lt;strong&gt;DocumentDB&lt;/strong&gt; for the project because of the many nested properties in the document, and I want to store the document as is. I didn't bother making the database and collection names very complex, and named both &lt;code&gt;products&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A second Lambda function called &lt;code&gt;changeStreamHandler&lt;/code&gt; watches the MongoDB change stream and &lt;strong&gt;indexes&lt;/strong&gt; the new product documents in &lt;a href="https://docs.aws.amazon.com/opensearch-service/latest/developerguide/serverless-overview.html" rel="noopener noreferrer"&gt;OpenSearch Serverless&lt;/a&gt; service. The function iterates over the &lt;code&gt;events&lt;/code&gt; &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/with-documentdb.html#docdb-sample-event" rel="noopener noreferrer"&gt;records&lt;/a&gt; and adds them to OpenSearch.&lt;/p&gt;

&lt;p&gt;Finally, users can invoke the &lt;code&gt;/search&lt;/code&gt; endpoint with a search expression to fetch matching product details. The &lt;code&gt;search&lt;/code&gt; function in my little POC performs a &lt;code&gt;multi_match&lt;/code&gt; query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...other function code&lt;/span&gt;
&lt;span class="c1"&gt;// Search in OpenSearch&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;opensearchClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;multi_match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// User's search expression&lt;/span&gt;
        &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;productName^3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;description^2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;brand&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;category&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ratings.averageRating&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;_score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the code above shows, the search expression looks for matching data in five &lt;code&gt;fields&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Requests to OpenSearch must be signed with &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html" rel="noopener noreferrer"&gt;AWS Signature V4&lt;/a&gt;. The &lt;a href="https://github.com/opensearch-project/opensearch-js/blob/da51f3d00fd2b3491129554070fea353089d25c3/README.md" rel="noopener noreferrer"&gt;OpenSearch project&lt;/a&gt; &lt;a href="https://www.npmjs.com/package/@opensearch-project/opensearch" rel="noopener noreferrer"&gt;npm package&lt;/a&gt; contains the &lt;code&gt;AwsSigv4Signer&lt;/code&gt; method, which makes the signature simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Client&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@opensearch-project/opensearch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AwsSigv4Signer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@opensearch-project/opensearch/aws&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nc"&gt;AwsSigv4Signer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eu-central-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aoss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Amazon OpenSearch Serverless&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's take a closer look at some key elements.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2. Detailed view
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Functions in the VPC.&lt;/strong&gt; It's a shame, but as of today, DocumentDB is only available in a &lt;strong&gt;cluster&lt;/strong&gt; format that runs managed instances. The cluster must be provisioned in at least two - preferably private - &lt;strong&gt;subnets&lt;/strong&gt; in a &lt;strong&gt;VPC&lt;/strong&gt;. There's currently no serverless version available!&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;product&lt;/code&gt; and &lt;code&gt;changeStreamHandler&lt;/code&gt; Lambda functions interact with the database. As such, we must provision them, possibly in the same VPC private subnets. (Let's not go into other VPC design options here.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security groups.&lt;/strong&gt; The functions' &lt;strong&gt;security groups&lt;/strong&gt; must allow &lt;strong&gt;outbound&lt;/strong&gt; rules to the security group assigned to the DocumentDB cluster. The DocumentDB security group must allow &lt;strong&gt;inbound&lt;/strong&gt; traffic on port &lt;code&gt;27017&lt;/code&gt; (the MongoDB default port) from the Lambda functions' security group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event source mapping.&lt;/strong&gt; Since the &lt;code&gt;changeStreamHandler&lt;/code&gt; reads events from the change stream, Lambda will use the &lt;strong&gt;polling&lt;/strong&gt; invocation model. We must configure an event source mapping, where, besides the usual stream settings, like &lt;code&gt;batchSize&lt;/code&gt; or &lt;code&gt;startingPosition&lt;/code&gt;, we must set the authentication option and specify the &lt;strong&gt;database&lt;/strong&gt; and &lt;strong&gt;collection&lt;/strong&gt; names the function will read events from. A sample CDK code with a low-level construct may look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventSourceMapping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ChangeStreamEventSourceMapping&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AWS::Lambda::EventSourceMapping&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;changeStreamLambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;EventSourceArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;docdbClusterArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;StartingPosition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LATEST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;BatchSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;SourceAccessConfigurations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BASIC_AUTH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="c1"&gt;// The ARN of the Secrets Manager secret&lt;/span&gt;
          &lt;span class="c1"&gt;// that holds the connection info&lt;/span&gt;
          &lt;span class="na"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;docdbSecret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secretArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;DocumentDBEventSourceConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;DatabaseName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;CollectionName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We must also specify the Secret Manager secret's &lt;strong&gt;ARN&lt;/strong&gt; containing the DocumentDB cluster connection information (username and password).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NAT Gateway.&lt;/strong&gt; For the functions to interact with DocumentDB via the event source mapping, we need to provision either a NAT Gateway or some &lt;strong&gt;VPC interface endpoints&lt;/strong&gt;. &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/with-documentdb.html#docdb-network" rel="noopener noreferrer"&gt;The documentation&lt;/a&gt; will describe what endpoints are required in more detail. For the sake of simplicity, I created a NAT Gateway, but it might not be the best option for some industries where companies need to comply with strict regulations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenSearch Serverless.&lt;/strong&gt; Great option for proofs-of-concept (like this one) or teams that don't want to manage OpenSearch clusters. It's a fully managed service where we don't have to worry about VPCs, security groups and NAT Gateways. The search function connects to OpenSearch Serverless through an &lt;code&gt;https&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permissions.&lt;/strong&gt; Lambda functions provisioned in a VPC must have specific permissions other than those they use to connect to DocumentDB. The Lambda service needs to create &lt;strong&gt;ENIs&lt;/strong&gt; - Elastic Network Interfaces, virtual network cards in the VPC -, so we must add the &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html" rel="noopener noreferrer"&gt;required permissions&lt;/a&gt; to the function's execution role.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;product&lt;/code&gt; function also needs &lt;strong&gt;Secrets Manager&lt;/strong&gt; (&lt;code&gt;secretsmanager:GetSecretValue&lt;/code&gt; and &lt;code&gt;secretsmanager:DescribeSecret&lt;/code&gt;) and &lt;a href="https://docs.aws.amazon.com/kms/latest/developerguide/overview.html" rel="noopener noreferrer"&gt;KMS&lt;/a&gt; (&lt;code&gt;kms:Decrypt&lt;/code&gt;) permissions since it needs to authenticate to DocumentDB.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Considerations
&lt;/h2&gt;

&lt;p&gt;The result is a simple event-driven application, where we use streams to react to MongoDB data changes.&lt;/p&gt;

&lt;p&gt;I want to highlight a couple of things, though.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1. Not production-ready
&lt;/h3&gt;

&lt;p&gt;As it might be clear from the discussion above, this article does &lt;strong&gt;not&lt;/strong&gt; cover the entire application.&lt;/p&gt;

&lt;p&gt;The application is only a POC and is &lt;strong&gt;not&lt;/strong&gt; production-ready. It only contains some core infrastructure elements.&lt;/p&gt;

&lt;p&gt;Error handling, retries and monitoring should also be implemented in production workloads.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2. Testing the app
&lt;/h3&gt;

&lt;p&gt;Besides the basic &lt;q&gt;let's call the endpoint and see what happens&lt;/q&gt; method, I tested the application with a script, which made a batch of 5 parallel requests per second to the &lt;code&gt;POST /product&lt;/code&gt; endpoint until 500 documents got uploaded to the database.&lt;/p&gt;

&lt;p&gt;The architecture elements scaled well when needed, and I didn't experience any bottlenecks.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3. Connecting to the database cluster
&lt;/h3&gt;

&lt;p&gt;If you are like me and prefer connecting to databases from the terminal vs using a GUI, you are probably familiar with the &lt;a href="https://www.mongodb.com/docs/mongodb-shell/" rel="noopener noreferrer"&gt;MongoDB Shell&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;But how can we connect to the DocumentDB cluster, which is provisioned in a private network, from outside?&lt;/p&gt;

&lt;p&gt;We have a couple of &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/connect_programmatically.html" rel="noopener noreferrer"&gt;options&lt;/a&gt;. One of them is to launch an EC2 instance in the same VPC, install &lt;code&gt;mongosh&lt;/code&gt; on the instance, and copy the cluster connection command from the DocumentDB page in the console.&lt;/p&gt;

&lt;p&gt;For example, we can then check which databases and collections have the change streams setting enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="na"&gt;$listChangeStreams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response will look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;waitedMS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;firstBatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DB_NAME&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;COLLECTION_NAME&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test.$cmd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;operationTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1747661570&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alternatively, you can use the usual GUI solutions like &lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/studio3t.html" rel="noopener noreferrer"&gt;Studio 3T&lt;/a&gt;. Instructions on connecting to DocumentDB can be found in the official documentation.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.4. Direct integration
&lt;/h3&gt;

&lt;p&gt;I discussed &lt;strong&gt;one&lt;/strong&gt; possible solution to connect OpenSearch and DocumentDB.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/opensearch-service/latest/developerguide/configure-client-docdb.html" rel="noopener noreferrer"&gt;OpenSearch Ingestion pipelines&lt;/a&gt; support DocumentDB as source and keep the database in sync with OpenSearch without using an intermediary compute resource, like the &lt;code&gt;changeStreamHandler&lt;/code&gt; function in this example. Ingestion pipelines support filtering, data transformation and enrichment, replacing the Lambda function with a managed feature.&lt;/p&gt;

&lt;p&gt;The need to control the business logic and cost considerations (ingestion pipelines vs running a Lambda function) can help make the right architecture decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.5. Other event-driven options
&lt;/h3&gt;

&lt;p&gt;Indexing documents in OpenSearch is not the only action we can perform by building on change streams.&lt;/p&gt;

&lt;p&gt;We can create &lt;strong&gt;fanout&lt;/strong&gt; patterns by having the Lambda function publish a message to an &lt;a href="https://docs.aws.amazon.com/sns/latest/dg/welcome.html" rel="noopener noreferrer"&gt;SNS topic&lt;/a&gt; or an &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-bus.html" rel="noopener noreferrer"&gt;EventBridge event bus&lt;/a&gt;, allowing other (micro)services to react to the MongoDB state changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Summary
&lt;/h2&gt;

&lt;p&gt;MongoDB change streams offer the option to implement event-driven patterns that react to document changes in the database.&lt;/p&gt;

&lt;p&gt;DocumentDB is a NoSQL database with MongoDB compatibility, where version 5.0.0 supports change streams.&lt;/p&gt;

&lt;p&gt;One implementation pattern is to index the newly created document in OpenSearch and provide users with a search box to perform advanced searches.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Further reading
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/opensearch-service/pricing/" rel="noopener noreferrer"&gt;Amazon OpenSearch Service Pricing&lt;/a&gt; - Ingestion pipelines pricing included&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/getting-started.html" rel="noopener noreferrer"&gt;Create your first Lambda function&lt;/a&gt; - If you need help in creating Lambda functions&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/db-cluster-create.html" rel="noopener noreferrer"&gt;Creating an Amazon DocumentDB cluster&lt;/a&gt; - I can't add more to the title&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>mongodb</category>
      <category>lambda</category>
    </item>
    <item>
      <title>Triggering multiple workflows with DynamoDB Streams</title>
      <dc:creator>Arpad Toth</dc:creator>
      <pubDate>Thu, 15 May 2025 06:50:34 +0000</pubDate>
      <link>https://dev.to/aws-builders/triggering-multiple-workflows-with-dynamodb-streams-38h1</link>
      <guid>https://dev.to/aws-builders/triggering-multiple-workflows-with-dynamodb-streams-38h1</guid>
      <description>&lt;p&gt;How can we trigger multiple workflows in response to database changes? One way is to design an event-driven architecture with a fanout pattern. DynamoDB Streams makes it easy to implement these architectures.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The scenario
&lt;/h2&gt;

&lt;p&gt;I love &lt;a href="https://aws.amazon.com/event-driven-architecture/" rel="noopener noreferrer"&gt;event-driven architectures&lt;/a&gt;. They have a natural flow, require minimal code, and are well-suited for microservices.&lt;/p&gt;

&lt;p&gt;A common use case I have encountered is that after a write is committed to the database, other services in the application need to trigger their business logic based on the new data.&lt;/p&gt;

&lt;p&gt;Developers often write code to make this work. A typical solution looks like this (pseudo-code):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;writeData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Write data and wait for the success response&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;databaseResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Publish a message to SNS&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;snsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PublishCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;TopicArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TOPIC_ARN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, the function persists the data to the database and waits for a success response. Then, it publishes a message to a service like &lt;a href="https://docs.aws.amazon.com/sns/latest/dg/welcome.html" rel="noopener noreferrer"&gt;SNS&lt;/a&gt;, where multiple subscribers can process the information.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjlm87db0eto89jfp9glf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjlm87db0eto89jfp9glf.png" alt="Multi-tasking function code" width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alternatively, the function might try to handle everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fanout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Send a message to an SQS queue&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sqsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SendMessageCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;QueueUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QUEUE1_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;MessageBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="c1"&gt;// Send a message to another SQS queue&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sqsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SendMessageCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;QueueUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QUEUE2_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;MessageBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="c1"&gt;// Start a Step Functions state machine execution&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sqsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SendMessageCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;QueueUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;QUEUE3_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;MessageBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="c1"&gt;// etc.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would avoid code like this. Whether these calls run in parallel or sequentially, you will need to implement robust error handling and retry mechanisms, and the code can quickly become complex, requiring significant debugging time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fppit3gjv5zr9iuko7xlz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fppit3gjv5zr9iuko7xlz.png" alt="Function code is multi-tasking again" width="800" height="797"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sometimes, writing such code is unavoidable. However, some AWS database services offer excellent support for event-driven solutions. With &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html" rel="noopener noreferrer"&gt;DynamoDB&lt;/a&gt;, you can capture data changes by enabling &lt;strong&gt;streams&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;DynamoDB offers two streaming models: &lt;strong&gt;DynamoDB Streams&lt;/strong&gt; and &lt;a href="https://docs.aws.amazon.com/streams/latest/dev/introduction.html" rel="noopener noreferrer"&gt;Kinesis Data Streams&lt;/a&gt; for DynamoDB.&lt;/p&gt;

&lt;p&gt;This article focuses on DynamoDB Streams and how to implement streamlined event-driven architectures by capturing data changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. DynamoDB Streams
&lt;/h2&gt;

&lt;p&gt;Wouldn’t it be great if the &lt;code&gt;writeData&lt;/code&gt; function’s only responsibility were to save new or updated items to DynamoDB, and other services in the application could receive notifications about new data and react independently?&lt;/p&gt;

&lt;p&gt;I’ll answer my own question: Yes, it would be great.&lt;/p&gt;

&lt;p&gt;DynamoDB Streams is designed for this purpose. Here are some key features:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Stores records for up to 24 hours.&lt;/li&gt;
  &lt;li&gt;Records appear &lt;b&gt;exactly once&lt;/b&gt; and leave the stream in the same order they entered.&lt;/li&gt;
  &lt;li&gt;Lambda reads from the stream are &lt;b&gt;free&lt;/b&gt; (this alone makes DynamoDB Streams appealing).&lt;/li&gt;
  &lt;li&gt;Supports up to &lt;b&gt;two concurrent consumers&lt;/b&gt; per shard for single-region (not global) tables via the DynamoDB Streams &lt;code&gt;GetRecords&lt;/code&gt; API. Reads from additional consumers may be throttled.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This sounds promising, except for the last point. Only two consumers? That limits us to basic fanout-like solutions.&lt;/p&gt;

&lt;p&gt;Despite the API read restriction, we can still use streams to trigger multiple downstream services and design fanout architectures.&lt;/p&gt;

&lt;p&gt;Let’s explore some solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Using Lambda functions
&lt;/h2&gt;

&lt;p&gt;I use &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" rel="noopener noreferrer"&gt;Lambda functions&lt;/a&gt; whenever possible, so I’m excited that pairing DynamoDB Streams with Lambda presents many opportunities.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1. Lambda as a consumer
&lt;/h3&gt;

&lt;p&gt;When Lambda is added as a stream consumer, the Lambda service handles everything through event-source mapping settings. You can configure options like batch size or an on-failure destination. Lambda uses a &lt;strong&gt;polling invocation model&lt;/strong&gt;, querying the stream four times per second.&lt;/p&gt;

&lt;p&gt;You can also configure up to five &lt;strong&gt;filter criteria&lt;/strong&gt; per Lambda function to match specific item attributes. Lambda only invokes the function when a matching record appears in the stream, eliminating the need for filtering logic in your code. This can save significant costs by avoiding invocations for irrelevant events.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.2. Fanout with Lambda
&lt;/h3&gt;

&lt;p&gt;How can we run multiple business logic processes on database change records?&lt;/p&gt;

&lt;p&gt;You can configure &lt;strong&gt;multiple&lt;/strong&gt; Lambda functions (more than two) to be triggered by the same DynamoDB stream. Each function receives and processes the &lt;strong&gt;same records&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyh0rruqmayp6qqif2u5l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyh0rruqmayp6qqif2u5l.png" alt="Lambda consumer with functions" width="800" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lambda functions don’t count as separate consumers, as the Lambda service acts as the consumer. When a new record is available (which is almost always, given the use cases of streams), Lambda invokes the functions simultaneously, scaling their concurrency as needed (and configured).&lt;/p&gt;

&lt;p&gt;DynamoDB automatically adjusts the number of shards, and you have no control over this.&lt;/p&gt;

&lt;p&gt;Fanout with Lambda functions? Absolutely!&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Other fanout options
&lt;/h2&gt;

&lt;p&gt;Not every use case suits Lambda functions, and not everyone prefers Lambda. How can we trigger multiple workflows that don’t rely on Lambda?&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. Intermediary service
&lt;/h3&gt;

&lt;p&gt;The two-concurrent-consumers-per-shard limit can be a bottleneck. A solution can be an &lt;strong&gt;intermediary&lt;/strong&gt; service that receives records from the stream and forwards them to another service responsible for triggering interested parties.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2. Fanout with EventBridge Pipes
&lt;/h3&gt;

&lt;p&gt;A great candidate for an intermediary service is &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-pipes.html" rel="noopener noreferrer"&gt;EventBridge Pipes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;EventBridge Pipes connects a &lt;strong&gt;source&lt;/strong&gt; to a &lt;strong&gt;target&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;DynamoDB Streams is a supported &lt;strong&gt;source&lt;/strong&gt; for a pipe, with direct integration, so no external tools (like a Lambda function) are needed. EventBridge polls the stream shards at four requests per second.&lt;/p&gt;

&lt;p&gt;For a fanout architecture, you can choose an &lt;strong&gt;SNS topic&lt;/strong&gt; or an &lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-bus.html" rel="noopener noreferrer"&gt;EventBridge event bus&lt;/a&gt; as the &lt;strong&gt;target&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;SNS supports a wide range of subscribers, including &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/welcome.html" rel="noopener noreferrer"&gt;SQS queues&lt;/a&gt;, Lambda functions, or email addresses.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsnwyouyj5jm3f94j2j60.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsnwyouyj5jm3f94j2j60.png" alt="EventBridge Pipes with SNS target" width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;EventBridge buses support up to five targets per rule, offering a smaller fanout than SNS.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpklq4kdp64clpix3zznq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpklq4kdp64clpix3zznq.png" alt="EventBridge Pipes with event bus target" width="800" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html" rel="noopener noreferrer"&gt;Step Functions&lt;/a&gt; &lt;strong&gt;state machine&lt;/strong&gt; is also a valid target for EventBridge Pipes. You can create fanout-like solutions using the &lt;code&gt;Parallel&lt;/code&gt; state, where different workflows run independently.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F88oq6smoqgboshwg4isd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F88oq6smoqgboshwg4isd.png" alt="EventBridge Pipes with Step Functions state machine target" width="800" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is an interesting solution worth exploring.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3. Fanout with a Lambda function
&lt;/h3&gt;

&lt;p&gt;Lambda &lt;strong&gt;functions&lt;/strong&gt; can also serve as intermediary services. A router function can act as the stream consumer, forwarding events to services like SNS or EventBridge after receiving a record.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5tkrvlqk2kxe8b0x2tpd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5tkrvlqk2kxe8b0x2tpd.png" alt="Lambda function publishes to a topic" width="800" height="282"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Advantages of using Lambda as an intermediary include simplicity and the control provided by custom code. It may also be more cost-effective than EventBridge Pipes.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4. If none of these work
&lt;/h3&gt;

&lt;p&gt;If none of these solutions fit your use case, I’m sorry to hear that!&lt;/p&gt;

&lt;p&gt;I believe a small Lambda intermediary function should suit most architectures. If not, you can retrieve stream records directly using the &lt;code&gt;GetRecords&lt;/code&gt; API and deploy code to poll the stream on any AWS compute service. Be mindful of the two-concurrent-consumers-per-shard rule when implementing custom solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Summary
&lt;/h2&gt;

&lt;p&gt;You can react to database changes in an event-driven way using DynamoDB Streams.&lt;/p&gt;

&lt;p&gt;The simplest approach is to use streams with Lambda functions. If Lambda isn’t suitable, EventBridge Pipes or a Lambda intermediary function can distribute data change events to an SNS topic or EventBridge event bus, where you can add multiple subscribers or targets.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Further Reading
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/streamsmain.html" rel="noopener noreferrer"&gt;Change data capture with Amazon DynamoDB&lt;/a&gt; - DynamoDB stream options&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ServiceQuotas.html#limits-dynamodb-streams" rel="noopener noreferrer"&gt;DynamoDB Streams limits&lt;/a&gt; - Throttling considerations&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/with-ddb-filtering.html" rel="noopener noreferrer"&gt;Using event filtering with a DynamoDB event source&lt;/a&gt; - Event filtering options in Lambda&lt;/p&gt;

</description>
      <category>aws</category>
      <category>eventdriven</category>
      <category>serverless</category>
      <category>dynamodb</category>
    </item>
    <item>
      <title>4 Cognito User Pools features you might not know about</title>
      <dc:creator>Arpad Toth</dc:creator>
      <pubDate>Thu, 10 Apr 2025 19:01:25 +0000</pubDate>
      <link>https://dev.to/aws-builders/4-cognito-user-pools-features-you-might-not-know-about-3f0c</link>
      <guid>https://dev.to/aws-builders/4-cognito-user-pools-features-you-might-not-know-about-3f0c</guid>
      <description>&lt;p&gt;Cognito User Pools is more than just a user directory. It's an ecosystem that tackles authentication edge cases and boosts development efficiency.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Cognito User Pools - beyond a simple user directory
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/getting-started-user-pools.html" rel="noopener noreferrer"&gt;Cognito User Pools&lt;/a&gt; is a fully managed, &lt;a href="https://openid.net/" rel="noopener noreferrer"&gt;OpenID Connect&lt;/a&gt;-compatible identity provider. It serves as a user directory service that handles authentication and authorization for &lt;strong&gt;application users&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Importantly, Cognito User Pools doesn’t manage access to AWS resources like S3 or DynamoDB. It’s designed for the mobile and web applications we build. With a user pool integrated into an app, our users can sign up, log in, and change passwords effortlessly, requiring minimal work on our end.&lt;/p&gt;

&lt;p&gt;Having a service like Cognito User Pools is a game-changer. Before using it, I built authentication workflows manually, and trust me, it was far from enjoyable. It’s much simpler to rely on a dedicated service that handles all the flows right out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Lesser-known features
&lt;/h2&gt;

&lt;p&gt;Beyond the basics, Cognito User Pools offers some lesser-known features that enhance the experience for both users and administrators.&lt;/p&gt;

&lt;p&gt;In this post, I’ll highlight four of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1. Modifying tokens
&lt;/h3&gt;

&lt;p&gt;As mentioned, Cognito User Pools aligns with the OpenID Connect standard, issuing an &lt;strong&gt;ID token&lt;/strong&gt; once a user successfully authenticates. It also provides &lt;strong&gt;access tokens&lt;/strong&gt;, making it compliant with &lt;a href="https://oauth.net/2/" rel="noopener noreferrer"&gt;OAuth 2.0&lt;/a&gt; standards.&lt;/p&gt;

&lt;p&gt;Tokens are expected, but did you know you can intercept the authentication flow and add custom properties to them?&lt;/p&gt;

&lt;p&gt;We can set up Cognito to trigger a &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" rel="noopener noreferrer"&gt;Lambda function&lt;/a&gt; at various stages of the &lt;a href="https://dev.to/aws-builders/manual-user-registration-approvals-in-multi-tenant-applications-with-step-functions-12ae" rel="noopener noreferrer"&gt;sign-up&lt;/a&gt; and sign-in processes. These functions can enrich both the &lt;a href="https://dev.to/aws-builders/implementing-access-control-on-api-gateway-endpoints-with-id-tokens-k9p" rel="noopener noreferrer"&gt;ID token&lt;/a&gt; and the &lt;a href="https://dev.to/aws-builders/using-customized-access-tokens-to-set-up-authorization-in-api-gateway-26go" rel="noopener noreferrer"&gt;access token&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This opens up a world of customization options for controlling app access. For example, we can embed custom data in the &lt;strong&gt;ID token&lt;/strong&gt; for the front-end client to use, enabling guards to restrict content. Alternatively, we can add custom scopes to the &lt;strong&gt;access token&lt;/strong&gt; and implement fine-grained access control in an &lt;a href="https://aws.amazon.com/api-gateway/" rel="noopener noreferrer"&gt;API Gateway&lt;/a&gt; API. All it takes is some Lambda function code, and Cognito triggers it at the right time.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2. Passkeys for login
&lt;/h3&gt;

&lt;p&gt;Cognito also lets us integrate &lt;strong&gt;passwordless&lt;/strong&gt; login into our applications!&lt;/p&gt;

&lt;p&gt;One option is using &lt;a href="https://dev.to/aws-builders/using-yubikeys-for-passwordless-authentication-in-cognito-user-pools-56ne" rel="noopener noreferrer"&gt;passkeys&lt;/a&gt;. YubiKeys are a popular choice, but password managers and operating system key storage options work seamlessly with Cognito too.&lt;/p&gt;

&lt;p&gt;Passwordless sign-in is getting more popular, and with Cognito, we can keep our apps ahead of the curve.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3. User existence error masking
&lt;/h3&gt;

&lt;p&gt;How does your app respond when someone tries to log in with a nonexistent username?&lt;/p&gt;

&lt;p&gt;One approach is to return a “User not found” error, but this tells the user they can keep guessing with different usernames.&lt;/p&gt;

&lt;p&gt;By enabling the &lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pool-managing-errors.html" rel="noopener noreferrer"&gt;Prevent user existence errors&lt;/a&gt; feature in the App client settings, Cognito displays a vague error like “The username or password is incorrect” when someone tries to log in with a nonexistent username.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3pkc0gcf2uym158isd0j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3pkc0gcf2uym158isd0j.png" alt="Prevent user existence errors" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This feature extends to passwordless sign-in too. When I set up the &lt;a href="https://dev.to/aws-builders/implementing-passwordless-sign-in-flow-with-email-in-cognito-109g" rel="noopener noreferrer"&gt;email verification code&lt;/a&gt; option and entered a nonexistent username, Cognito displayed the standard, expected message on the next page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr1gg3n29bjbpv7sax3qj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr1gg3n29bjbpv7sax3qj.png" alt="Simulated message" width="800" height="916"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, what’s happening here? How does the “Prevent user existence errors” feature play out?&lt;/p&gt;

&lt;p&gt;It’s all about the email address - it’s fake.&lt;/p&gt;

&lt;p&gt;At first, I thought this was a bug and that Cognito might have sent a verification code to some random stranger’s email. But the truth is, when the user doesn’t exist, Cognito shows a &lt;strong&gt;simulated message&lt;/strong&gt; with a dummy email address and never sends the validation code.&lt;/p&gt;

&lt;p&gt;Big thanks to AWS technical support and the Cognito team for clearing this up! 🙌&lt;/p&gt;

&lt;h3&gt;
  
  
  2.4. Customizable login page
&lt;/h3&gt;

&lt;p&gt;Cognito offers hosted authentication pages known as the hosted UI. Recently, they rolled out &lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-managed-login.html" rel="noopener noreferrer"&gt;managed login&lt;/a&gt;, an updated version of the classic hosted UI.&lt;/p&gt;

&lt;p&gt;This managed authentication page is a lifesaver. It means we don’t have to build login and sign-up forms from scratch on the front end.&lt;/p&gt;

&lt;p&gt;But there’s more! We can &lt;strong&gt;customize&lt;/strong&gt; the &lt;strong&gt;managed login&lt;/strong&gt; page using a no-code visual editor called the &lt;strong&gt;branding designer&lt;/strong&gt;, which lets us tweak every element of the login form.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa88s1cl0ied0uie563m0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fa88s1cl0ied0uie563m0.png" alt="Customizable components" width="800" height="365"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can adjust spacing and border-radius, and add custom logos or background images, among other options. If you prefer, you can still upload a custom CSS file with the classic hosted UI. Switching between the two is easy if needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Summary
&lt;/h2&gt;

&lt;p&gt;This short post explored four lesser-known Cognito User Pools features: token modification, passkey-based passwordless authentication, user existence error masking, and customizable managed login pages.&lt;/p&gt;

&lt;p&gt;These features have made my life easier when integrating Cognito into my applications. How about you?&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>cognito</category>
    </item>
    <item>
      <title>Verifying Cognito access tokens - Comparing three JWT packages for Lambda authorizers</title>
      <dc:creator>Arpad Toth</dc:creator>
      <pubDate>Thu, 03 Apr 2025 07:08:06 +0000</pubDate>
      <link>https://dev.to/aws-builders/verifying-cognito-access-tokens-comparing-three-jwt-packages-for-lambda-authorizers-367b</link>
      <guid>https://dev.to/aws-builders/verifying-cognito-access-tokens-comparing-three-jwt-packages-for-lambda-authorizers-367b</guid>
      <description>&lt;p&gt;When using Cognito access tokens to secure our API, we can choose from several JSON Web Token packages to verify tokens in Lambda authorizer functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The scenario
&lt;/h2&gt;

&lt;p&gt;When the built-in Amazon &lt;a href="https://aws.amazon.com/api-gateway/" rel="noopener noreferrer"&gt;API Gateway&lt;/a&gt; &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-control-access-to-api.html" rel="noopener noreferrer"&gt;authorization&lt;/a&gt; methods don’t fully meet our needs, we can set up &lt;a href="https://aws.amazon.com/lambda" rel="noopener noreferrer"&gt;Lambda&lt;/a&gt; authorizers to manage the access control process. Even when using &lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/getting-started-user-pools.html" rel="noopener noreferrer"&gt;Cognito user pools&lt;/a&gt; and Cognito &lt;strong&gt;access tokens&lt;/strong&gt;, there may still be a need for &lt;a href="https://arpadt.com/articles/path-user-matching-auth" rel="noopener noreferrer"&gt;custom authorization logic&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The Lambda authorizer code &lt;strong&gt;decodes&lt;/strong&gt; and &lt;strong&gt;verifies&lt;/strong&gt; the token, and its business logic determines whether the request should proceed to the backend or be denied. Cognito access tokens are &lt;a href="https://jwt.io/" rel="noopener noreferrer"&gt;JSON Web Tokens (JWTs)&lt;/a&gt;, and to simplify our coding, we might opt for an external package to handle token verification.&lt;/p&gt;

&lt;p&gt;This article compares three JWT packages designed for &lt;a href="https://nodejs.org/en" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; and &lt;a href="https://www.typescriptlang.org/" rel="noopener noreferrer"&gt;TypeScript&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Requirements
&lt;/h2&gt;

&lt;p&gt;The comparison and final verdict are based on my specific requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lambda authorizers are necessary due to unique validation needs.&lt;/li&gt;
&lt;li&gt;I use Cognito access tokens with scopes, which I verify in the authorizer (not covered in this article).&lt;/li&gt;
&lt;li&gt;The authorizer should immediately return a deny policy if called with invalid, expired, or non-access tokens.&lt;/li&gt;
&lt;li&gt;I rely on certain token properties in the authorizer logic, so decoding and verification are required.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Pre-requisites
&lt;/h2&gt;

&lt;p&gt;This post will not go into the details of setting up the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Cognito user pool.&lt;/li&gt;
&lt;li&gt;A REST-type API Gateway.&lt;/li&gt;
&lt;li&gt;A Lambda authorizer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Links to relevant documentation will be provided at the end for those who need them.&lt;/p&gt;

&lt;p&gt;As always, the solution I have chosen works for my use case, but your opinion or experience might differ.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Comparison
&lt;/h2&gt;

&lt;p&gt;I explored three packages: &lt;a href="https://www.npmjs.com/package/aws-jwt-verify" rel="noopener noreferrer"&gt;AWS JWT Verify&lt;/a&gt;, &lt;a href="https://www.npmjs.com/package/jose" rel="noopener noreferrer"&gt;jose&lt;/a&gt;, and &lt;a href="https://www.npmjs.com/package/jsonwebtoken" rel="noopener noreferrer"&gt;jsonwebtoken&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The code examples I provide focus solely on the verification process. The custom logic for why the authorizer exists is omitted - you can substitute it with your logic. I also use utility functions to group similar logic, but I have left them out to keep things concise, so some snippets include pseudo-code.&lt;/p&gt;

&lt;p&gt;Results in the &lt;strong&gt;Performance&lt;/strong&gt; sections are based on tests with a 512 MB Lambda authorizer memory configuration.&lt;/p&gt;

&lt;p&gt;After this necessary but lengthy introduction, let’s dive into the packages.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. aws-jwt-verify
&lt;/h3&gt;

&lt;p&gt;Since I’m working with Cognito tokens, I started with &lt;strong&gt;AWS JWT Verify&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Purpose&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Maintained by AWS, this package is specifically designed to verify JWTs issued by Cognito.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here’s how the token verification flow might look with &lt;code&gt;aws-jwt-verify&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CognitoJwtVerifier&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-jwt-verify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayRequestAuthorizerEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CognitoAccessTokenPayload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-jwt-verify/jwt-model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// import util methods&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Instantiate the verifier outside the handler&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;CognitoJwtVerifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;userPoolId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;USER_POOL_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tokenUse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;access&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APP_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayRequestAuthorizerEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;methodArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// util function to extract the JWT from the header&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractAuthorizationToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// util function to generate the DENY IAM policy&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;generatePolicy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Deny&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;principal_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Some error message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CognitoAccessTokenPayload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 2. Use the verifier's "verify" method&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;generatePolicy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Deny&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;principal_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Some error message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// custom endpoint access control logic to verify scope here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some key points to note.&lt;/p&gt;

&lt;p&gt;First, we create a Cognito JWT verifier (1) by specifying the &lt;strong&gt;user pool ID&lt;/strong&gt;, &lt;strong&gt;app client ID&lt;/strong&gt;, and the token type to verify (&lt;code&gt;access&lt;/code&gt;, &lt;code&gt;id&lt;/code&gt;, or &lt;code&gt;null&lt;/code&gt;). Setting &lt;code&gt;null&lt;/code&gt; skips checking the &lt;code&gt;token_use&lt;/code&gt; property. The authorizer pulls the user pool and app client ID from &lt;strong&gt;environment variables&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It’s best to instantiate the verifier &lt;strong&gt;outside the handler&lt;/strong&gt; function. Since the user pool and app client ID are static and their values don’t depend on the &lt;code&gt;event&lt;/code&gt; payload, this approach improves the Lambda function’s runtime. Code outside the &lt;code&gt;handler&lt;/code&gt; is pre-loaded in the execution environment.&lt;/p&gt;

&lt;p&gt;The core logic relies on the verifier’s verify method (2), which returns the &lt;strong&gt;token payload&lt;/strong&gt; upon successful verification. As shown, no extra logic is needed. The package handles everything internally, keeping the code clean and readable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My tests showed an initial &lt;strong&gt;cold start&lt;/strong&gt; of 650–670 milliseconds. The first invocation took around 200 milliseconds, with subsequent calls typically under 100 ms, regardless of the verification result. I even saw a few single-digit durations, which was impressive!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handles all Cognito-specific validation&lt;/li&gt;
&lt;li&gt;Built-in TypeScript support&lt;/li&gt;
&lt;li&gt;Minimal configuration and no extra methods required&lt;/li&gt;
&lt;li&gt;Fetches and caches JSON Web Key Set under the hood&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Limited to AWS tokens&lt;/li&gt;
&lt;li&gt;Limited algorithm support (AWS use cases)&lt;/li&gt;
&lt;li&gt;Restrictions apply when used with non-AWS identity providers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best to use it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;aws-jwt-verify&lt;/code&gt; package shines when your application relies on &lt;strong&gt;Cognito&lt;/strong&gt; as an identity provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experience&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I found this package fast and straightforward to use. It handles common verification scenarios (expired tokens, an ID token is sent instead of the access token) effectively and manages caching internally.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2. jose
&lt;/h3&gt;

&lt;p&gt;Next up, I tested &lt;strong&gt;jose&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Purpose&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;jose&lt;/code&gt; is a well-maintained package that fully implements the &lt;strong&gt;JavaScript Object Signing and Encryption (JOSE)&lt;/strong&gt; specification. It’s larger than &lt;code&gt;aws-jwt-verify&lt;/code&gt; and offers more features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here’s how I implemented &lt;code&gt;jose&lt;/code&gt; for Cognito access tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayRequestAuthorizerEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createRemoteJWKSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;jwtVerify&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jose&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CognitoAccessTokenPayload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-jwt-verify/jwt-model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// import util methods&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;JSON_WEB_KEY_SET_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ISSUER_URL&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Cache the JSON Web Key Set&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cachedJWKS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;createRemoteJWKSet&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayRequestAuthorizerEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;methodArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// util function to extract the JWT from the header&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractAuthorizationToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;generatePolicy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Deny&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;principal_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Some error message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Don’t feed the verifier with the token if it’s not an access token&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decodedPayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decodedPayload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token_use&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;access&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;generatePolicy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Deny&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;principal_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Some error message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CognitoAccessTokenPayload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 3. Verifier util method is created to make the code&lt;/span&gt;
    &lt;span class="c1"&gt;// more readable&lt;/span&gt;
    &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;jwkUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON_WEB_KEY_SET_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;issuerUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ISSUER_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// util method to handle different types of verification errors&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;tokenVerificationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// custom endpoint access control logic to verify scope here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Verifier utility function&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;VerifyTokenProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;jwkUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;issuerUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;VerifyTokenProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cachedJWKS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cachedJWKS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRemoteJWKSet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jwkUrl&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verifyResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;jwtVerify&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CognitoAccessTokenPayload&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;cachedJWKS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issuerUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// the user pool’s&lt;/span&gt;
      &lt;span class="na"&gt;maxTokenAge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// (4)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;verifyResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we &lt;strong&gt;cache&lt;/strong&gt; the function that resolves the JSON Web Key Set (JWKS) from the Cognito JWKS endpoint (1). This prevents repeated calls to the endpoint for every authorizer invocation.&lt;/p&gt;

&lt;p&gt;Next, I ensured the authorizer exits early if the token is &lt;strong&gt;not&lt;/strong&gt; an access token (2). Since only access tokens include the &lt;code&gt;scope&lt;/code&gt; property, there’s no point in proceeding with an incorrect token type.&lt;/p&gt;

&lt;p&gt;The token verification happens in the &lt;code&gt;verifyToken&lt;/code&gt; utility function (3). It checks for a local JWKS cache and downloads it from the remote endpoint if needed.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;jwtVerify&lt;/code&gt; function validates the token and returns the decoded payload on success. I discovered that without specifying &lt;code&gt;maxTokenAge&lt;/code&gt;, the verifier hangs until the authorizer times out when given an expired token (4).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Lambda cold start was similar to &lt;code&gt;aws-jwt-verify&lt;/code&gt;. The first call took about 3.5 seconds due to the external API call to the Cognito JWKS endpoint. Subsequent calls ranged from 150–250 milliseconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete implementation of JOSE specifications&lt;/li&gt;
&lt;li&gt;Supports a wide range of algorithms&lt;/li&gt;
&lt;li&gt;TypeScript support&lt;/li&gt;
&lt;li&gt;Supports both JWS (signing) and JWE (encryption)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Larger package&lt;/li&gt;
&lt;li&gt;May be overkill for simple JWT use cases&lt;/li&gt;
&lt;li&gt;More complex API, requiring additional configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best to use it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;jose&lt;/code&gt; package excels with complex security needs, token creation, or non-AWS JWT implementations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experience&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Initially, the authorizer timed out after 6 seconds with invalid or missing tokens. I realized I needed more memory. Calls consistently used over 300 MB of RAM, so configure the Lambda accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3. jsonwebtoken
&lt;/h3&gt;

&lt;p&gt;The final package I evaluated was &lt;strong&gt;jsonwebtoken&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Purpose&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The last commit to the &lt;code&gt;jsonwebtoken&lt;/code&gt; repository was two years ago. I didn’t dig into the source code to assess the need for updates.  With nearly 20 million weekly downloads, this popular package maintains the strong trust of developers and other libraries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here’s how I implemented the logic with &lt;code&gt;jsonwebtoken&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;APIGatewayAuthorizerResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;APIGatewayRequestAuthorizerEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verify&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jsonwebtoken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;jwkToPem&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jwk-to-pem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CognitoAccessTokenPayload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-jwt-verify/jwt-model&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// import util methods&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;JSONWebKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwkToPem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWK&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;kid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;JWKSResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSONWebKey&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Implement caching for better performance&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;jwksCache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSONWebKey&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jwksPemCache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayRequestAuthorizerEvent&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;APIGatewayAuthorizerResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;methodArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// util function to extract the JWT from the header&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractAuthorizationToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;generatePolicy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Deny&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;principal_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Some error message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// built-in method to decode the JWT&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decodedToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Get the issuer from environment variables&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;issuer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cognito-idp.eu-central-1.amazonaws.com/USER_POOL_ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="na"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 2. Get the public key using a util function&lt;/span&gt;
    &lt;span class="nx"&gt;publicKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getPublicKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decodedToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;generatePolicy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Deny&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;principal_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Some error message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Verify the token&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;algorithms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RS256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;CognitoAccessTokenPayload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;generatePolicy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Deny&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;principal_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Some error message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;verified&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// custom endpoint access control logic to verify scope here&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can implement in-memory caching to enhance the authorizer’s performance (1).&lt;/p&gt;

&lt;p&gt;Then, we fetch the public key from the Cognito JWKS endpoint using a utility function (2). Here’s what &lt;code&gt;getPublicKey&lt;/code&gt; might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getJwks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jwksCache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;jwksCache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;JWKSResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;jwksCache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;jwksCache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getPublicKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;kid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jwksPemCache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kid&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;jwksPemCache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kid&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jwks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getJwks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kid&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;kid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Public key not found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jwkToPem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;jwksPemCache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use &lt;code&gt;axios&lt;/code&gt;, a popular HTTP client, to retrieve the key set.&lt;/p&gt;

&lt;p&gt;Finally, the built-in &lt;code&gt;verify&lt;/code&gt; method checks the token’s validity (3).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Lambda cold start was comparable to the other packages. The first call lasted 250–300 milliseconds, with subsequent (cached) calls often under 100 milliseconds. Peak memory usage hovered around 180 MB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Widely used, popular package with strong community support&lt;/li&gt;
&lt;li&gt;General-purpose JWT library flexible enough for custom JWT implementations&lt;/li&gt;
&lt;li&gt;Built-in methods for token decoding and verification&lt;/li&gt;
&lt;li&gt;Offers greater control over the verification process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In-memory caching is recommended to boost performance&lt;/li&gt;
&lt;li&gt;Requires extra code for full functionality&lt;/li&gt;
&lt;li&gt;You need to implement logic to fetch the key set&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best to use it&lt;/strong&gt;&lt;br&gt;
Use &lt;code&gt;jsonwebtoken&lt;/code&gt; for simple JWTs from Cognito or non-AWS identity providers. It’s ideal if you want a lightweight, general-purpose package with built-in methods.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experience&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I used &lt;code&gt;jsonwebtoken&lt;/code&gt; before, so its methods were familiar. It’s popular across frontend and backend development, and many packages depend on it. It performed reliably, though I had to invest some effort to match the logic of the other packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The verdict
&lt;/h2&gt;

&lt;p&gt;After testing all three packages, I chose &lt;code&gt;aws-jwt-verify&lt;/code&gt; for my project with Cognito access tokens.&lt;/p&gt;

&lt;p&gt;The deciding factor was its minimal code requirements. It also proved fastest in my tests, though for my use case, the difference between an 80-millisecond and a 200-millisecond response is negligible.&lt;/p&gt;

&lt;p&gt;For all packages, allocate sufficient memory to the authorizer function. I found 256 MB insufficient as functions often timed out with invalid tokens. I recommend at least 512 MB of RAM for each package.&lt;/p&gt;

&lt;p&gt;These packages are not limited to Lambda. You can use them to validate tokens in &lt;a href="https://www.npmjs.com/package/express" rel="noopener noreferrer"&gt;Express&lt;/a&gt; servers running on &lt;a href="https://aws.amazon.com/ec2/" rel="noopener noreferrer"&gt;EC2&lt;/a&gt; instances or containers, for example.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Summary
&lt;/h2&gt;

&lt;p&gt;Multiple npm packages can verify JWTs.&lt;/p&gt;

&lt;p&gt;For a Cognito user pool as an identity provider, &lt;code&gt;aws-jwt-verify&lt;/code&gt; offers a simple, lightweight solution.&lt;/p&gt;

&lt;p&gt;For general JWT verification, &lt;code&gt;jsonwebtoken&lt;/code&gt; is a solid choice. For advanced verification logic, consider &lt;code&gt;jose&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Further reading
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/getting-started-with-cognito-user-pools.html" rel="noopener noreferrer"&gt;Getting started with user pools&lt;/a&gt; - How to create a user pool with an app client&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started.html" rel="noopener noreferrer"&gt;Get started with API Gateway&lt;/a&gt; - How to create an API Gateway resource&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/getting-started.html" rel="noopener noreferrer"&gt;Create your first function&lt;/a&gt; - AWS Lambda "Getting started"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html" rel="noopener noreferrer"&gt;Verifying a JSON Web Token&lt;/a&gt; - What aws-jwt-verify does for token validation&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>serverless</category>
      <category>node</category>
    </item>
    <item>
      <title>Implementing advanced authorization with AWS Lambda for endpoint-specific access</title>
      <dc:creator>Arpad Toth</dc:creator>
      <pubDate>Thu, 27 Mar 2025 15:12:38 +0000</pubDate>
      <link>https://dev.to/aws-builders/implementing-advanced-authorization-with-aws-lambda-for-endpoint-specific-access-4g4p</link>
      <guid>https://dev.to/aws-builders/implementing-advanced-authorization-with-aws-lambda-for-endpoint-specific-access-4g4p</guid>
      <description>&lt;p&gt;When handling more complex authorization patterns, we can implement the necessary logic using Cognito Lambda triggers and authorizer functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The scenario
&lt;/h2&gt;

&lt;p&gt;I’m working on a maths contest application where participants move from one checkpoint to another, solving engaging maths puzzles. Each checkpoint has a task assigned to teams based on their year group.&lt;/p&gt;

&lt;p&gt;Checkpoints are staffed by invigilators or &lt;strong&gt;supervisors&lt;/strong&gt; who give the tasks to participants. Students step aside to solve the task, then share their - hopefully - correct answer with the supervisor, who logs it into the application.&lt;/p&gt;

&lt;p&gt;Naturally, the application runs on AWS serverless infrastructure. 😄&lt;/p&gt;

&lt;p&gt;One key requirement is that supervisors at any checkpoint can only access tasks assigned to their specific checkpoint. They should not see puzzles from other checkpoints.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://aws.amazon.com/api-gateway/" rel="noopener noreferrer"&gt;API Gateway&lt;/a&gt; includes an endpoint structured like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;API_URL/checkpoints/:id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This endpoint returns data for the application to display about the corresponding checkpoint. The application has many endpoints, but, in this article, I will focus on the &lt;code&gt;/checkpoints/:id&lt;/code&gt; endpoint to explain the solution.&lt;/p&gt;

&lt;p&gt;So, supervisors at &lt;strong&gt;Checkpoint 1&lt;/strong&gt; can only call &lt;code&gt;/checkpoints/1&lt;/code&gt;, those at &lt;strong&gt;Checkpoint 2&lt;/strong&gt; can call &lt;code&gt;/checkpoints/2&lt;/code&gt;, and so forth.&lt;/p&gt;

&lt;p&gt;The application uses a &lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/getting-started-user-pools.html" rel="noopener noreferrer"&gt;Cognito user pool&lt;/a&gt; to manage users, including the supervisors.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Challenges
&lt;/h2&gt;

&lt;p&gt;As I planned the solution for this scenario, I encountered several challenges. I will address the following ones below.&lt;/p&gt;

&lt;h3&gt;
  
  
  2.1. Endpoint protection
&lt;/h3&gt;

&lt;p&gt;Supervisors, regardless of the checkpoint, call the &lt;strong&gt;same&lt;/strong&gt; endpoint, with the &lt;code&gt;:id&lt;/code&gt; &lt;strong&gt;path parameter&lt;/strong&gt; being the only difference. How can I implement endpoint authorization to reject calls when the path parameter does not match the supervisor’s assigned checkpoint?&lt;/p&gt;

&lt;h3&gt;
  
  
  2.2. Client-side
&lt;/h3&gt;

&lt;p&gt;The client &lt;strong&gt;dynamically&lt;/strong&gt; adds the path parameter to the URL based on the supervisor’s checkpoint. How does the client determine which path parameter to use when making the call?&lt;/p&gt;

&lt;h3&gt;
  
  
  2.3. Consistency
&lt;/h3&gt;

&lt;p&gt;The client needs to call &lt;strong&gt;all endpoints&lt;/strong&gt;, including &lt;code&gt;/checkpoints/:id&lt;/code&gt;, using an &lt;strong&gt;access token&lt;/strong&gt; as outlined by &lt;a href="https://oauth.net/2/" rel="noopener noreferrer"&gt;OAuth 2.0&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Pre-requisites
&lt;/h2&gt;

&lt;p&gt;This post won't go into detail about how to create and configure the following:&lt;/p&gt;

&lt;ul&gt;
    &lt;li&gt;A Cognito user pool, groups and a pre-token generation trigger.&lt;/li&gt;
    &lt;li&gt;An app client with resource servers and scopes.&lt;/li&gt;
    &lt;li&gt;A REST-type API Gateway and its integrations.&lt;/li&gt;
    &lt;li&gt;Lambda functions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Links to the relevant documentation pages will be provided at the end of the post for those who need them.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Solution
&lt;/h2&gt;

&lt;p&gt;Here’s &lt;strong&gt;one&lt;/strong&gt; solution that works for my scenario.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. Overview
&lt;/h3&gt;

&lt;p&gt;The solution relies on &lt;strong&gt;two&lt;/strong&gt; &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" rel="noopener noreferrer"&gt;Lambda functions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F03hsf6v1tg86mg0fpx4f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F03hsf6v1tg86mg0fpx4f.png" alt="Authorization flow" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first function triggers after user authentication, but &lt;strong&gt;before&lt;/strong&gt; the user pool issues tokens. The second serves as the endpoint authorizer.&lt;/p&gt;

&lt;p&gt;Before implementing these, though, we need to assign supervisors to the appropriate Cognito &lt;strong&gt;groups&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2. Groups
&lt;/h3&gt;

&lt;p&gt;I opted for straightforward group names for supervisor assignments: &lt;code&gt;checkpoint-1&lt;/code&gt;, &lt;code&gt;checkpoint-2&lt;/code&gt;, and so on. Cognito allows up to 10,000 groups per user pool, which should be plenty unless our maths contest grows to an enormous scale.&lt;/p&gt;

&lt;p&gt;Supervisors at &lt;strong&gt;Checkpoint 1&lt;/strong&gt; are added to the &lt;code&gt;checkpoint-1&lt;/code&gt; group, and the same pattern applies until all supervisors are assigned to their respective groups.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3. Resource servers
&lt;/h3&gt;

&lt;p&gt;I configured a dedicated resource server and &lt;strong&gt;custom scopes&lt;/strong&gt; in the user pool for supervisor authorization management.&lt;/p&gt;

&lt;p&gt;I won’t explain the setup process here - I covered resource servers and custom scopes in a &lt;a href="https://dev.to/aws-builders/controlling-access-in-service-to-service-communications-with-cognito-part-1-5dc1" rel="noopener noreferrer"&gt;separate article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The custom scopes follow this format: &lt;code&gt;checkpoint/1&lt;/code&gt;, &lt;code&gt;checkpoint/2&lt;/code&gt;, and so on. Supervisors at &lt;strong&gt;Checkpoint 1&lt;/strong&gt; receive the &lt;code&gt;checkpoint/1&lt;/code&gt; scope, those at &lt;strong&gt;Checkpoint 2&lt;/strong&gt; get &lt;code&gt;checkpoint/2&lt;/code&gt;, and the pattern continues for all checkpoints.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4. Pre-token generation trigger
&lt;/h3&gt;

&lt;p&gt;We should tackle two tasks: let the client know the correct &lt;strong&gt;path parameter&lt;/strong&gt; and attach the &lt;strong&gt;matching scope&lt;/strong&gt; to the access token.&lt;/p&gt;

&lt;p&gt;A pre-token generation Lambda trigger can handle both. Once connected to the user pool, Cognito calls the Lambda function with a payload like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c1"&gt;// Some properties are omitted for brevity&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eu-central-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userPoolId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USER_POOL_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USER_NAME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"userAttributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USER_ID_IN_COGNITO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"groupConfiguration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"groupsToOverride"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="c1"&gt;// WE NEED THIS:&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"checkpoint-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scopes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"openid"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"claimsAndScopeOverrideDetails"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the pre-token generation Lambda trigger receives the supervisor’s assigned checkpoint! The function can then add this information to the &lt;strong&gt;ID&lt;/strong&gt; and &lt;strong&gt;access&lt;/strong&gt; tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ID tokens&lt;/strong&gt; carry details about the authenticated user. We can include a custom &lt;code&gt;checkpoint_id&lt;/code&gt; variable to store the checkpoint ID. The front-end client can pull this ID from the token and dynamically insert it as a path parameter in the URL.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Access tokens&lt;/strong&gt; contain authorization details in the &lt;code&gt;scope&lt;/code&gt; property. The pre-token generation trigger can add the relevant scope to the access token.&lt;/p&gt;

&lt;p&gt;Here’s what the function’s (pseudo) code might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Generate scopes&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groupConfiguration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groupsToOverride&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userScopes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateUserScopesFromCognitoGroups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Get the checkpoint ID for supervisors&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkpointId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractCheckpointIdFromScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userScopes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Add the info to the tokens&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;claimsAndScopeOverrideDetails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 3.a. Checkpoint ID to the ID token&lt;/span&gt;
        &lt;span class="na"&gt;idTokenGeneration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;claimsToAddOrOverride&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;checkpoint_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;checkpointId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="c1"&gt;// 3.b. Scopes to the access token&lt;/span&gt;
        &lt;span class="na"&gt;accessTokenGeneration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;scopesToAdd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;userScopes&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;generateUserScopesFromCognitoGroups&lt;/code&gt; utility converts a Cognito group name like &lt;code&gt;checkpoint-1&lt;/code&gt; into a scope format, &lt;code&gt;checkpoint/1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The extractCheckpointIdFromScope utility pulls the checkpoint ID &lt;code&gt;1&lt;/code&gt; from &lt;code&gt;checkpoint-1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once the function runs, the ID token includes this property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="nl"&gt;"checkpoint_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c1"&gt;// other ID token properties&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The access token reflects the corresponding checkpoint scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openid checkpoint/1"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c1"&gt;// other access token properties&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With these enriched tokens, the client can extract the &lt;code&gt;checkpoint_id&lt;/code&gt; from the &lt;strong&gt;ID token&lt;/strong&gt;, append it to the URL path, and call the endpoint with the &lt;strong&gt;access token&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.5. Lambda authorizer
&lt;/h3&gt;

&lt;p&gt;Now, the client calls the correct URL with the checkpoint ID in the path and the access token in the &lt;code&gt;Authorization&lt;/code&gt; header. The final step is to confirm that the checkpoint ID in the endpoint matches the scope in the token.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-integrate-with-cognito.html" rel="noopener noreferrer"&gt;Cognito authorizers&lt;/a&gt; are too rigid for this case. Using one would require attaching all checkpoint scopes to the endpoint, allowing any supervisor to call it with any checkpoint ID.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html" rel="noopener noreferrer"&gt;Lambda authorizers&lt;/a&gt; work well for &lt;strong&gt;custom authorization&lt;/strong&gt; logic. They are standard Lambda functions that receive specific authorizer payloads from API Gateway.&lt;/p&gt;

&lt;p&gt;Of the two payload types, &lt;code&gt;TOKEN&lt;/code&gt; and &lt;code&gt;REQUEST&lt;/code&gt;, I chose &lt;code&gt;REQUEST&lt;/code&gt; for this scenario. I have covered &lt;code&gt;TOKEN&lt;/code&gt;-type payloads &lt;a href="https://arpadt.com/articles/token-based-lambda-authorizer" rel="noopener noreferrer"&gt;elsewhere&lt;/a&gt;, so I will focus on &lt;code&gt;REQUEST&lt;/code&gt; payloads here.&lt;/p&gt;

&lt;p&gt;A Lambda authorizer with a &lt;code&gt;REQUEST&lt;/code&gt; payload gets an event object like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c1"&gt;// properties are omitted for brevity&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REQUEST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"methodArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:execute-api:eu-central-1:ACCOUNT_ID:API_ID/STAGE_NAME/GET/checkpoints/1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/checkpoints/{id}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/checkpoints/1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"httpMethod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c1"&gt;// WE NEED THIS&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer ACCESS_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pathParameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c1"&gt;// AND THIS&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stageVariables"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestContext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c1"&gt;// lots of properties&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event includes everything the authorizer needs to compare the &lt;code&gt;:id&lt;/code&gt; path parameter with the access token’s scope.&lt;/p&gt;

&lt;p&gt;Here’s a snippet of the authorizer’s (pseudo) code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pathParameters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;methodArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 1. Get the token from the Authorization header&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractAuthorizationToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// 2. Verify and decode the token&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenPayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verifyAccessToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Get the "sub" and "scope" properties from the token&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// 4. Get the checkpoint ID from the invoked URL path&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkPointIdInPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nf"&gt;extractCheckpointIdFromPathParameters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pathParameters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 5. Compare the checkpoint IDs from the path and the token&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;matchingCheckpointIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nf"&gt;matchPathParameterIdToScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checkPointIdInPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// 6. Return IAM policies as per the result&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;matchingCheckpointIds&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;generatePolicy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALLOW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;generatePolicy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DENY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Util function to generate the policy&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generatePolicy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;principalId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;policyDocument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2012-10-17&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;execute-api:Invoke&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These steps outline the logic described earlier.&lt;/p&gt;

&lt;p&gt;The authorizer function &lt;strong&gt;must&lt;/strong&gt; return an IAM policy. If the checkpoint IDs match, the policy will &lt;code&gt;Allow&lt;/code&gt; the request to proceed. If not, it will &lt;code&gt;Deny&lt;/code&gt; it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;principalId&lt;/code&gt; that identifies the user making the call is a mandatory attribute. The &lt;code&gt;sub&lt;/code&gt; property from the token, the user’s unique ID in the user pool, serves this purpose and identifies the supervisor.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Considerations
&lt;/h2&gt;

&lt;p&gt;As noted, Cognito groups are a practical way to organize supervisors by their checkpoint assignments.&lt;/p&gt;

&lt;p&gt;If we wanted to adapt this approach for an application with a larger user base, creating and managing groups might become impractical.&lt;/p&gt;

&lt;p&gt;In that case, we could store user details and their properties in a separate database like &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html" rel="noopener noreferrer"&gt;DynamoDB&lt;/a&gt;. The pre-token trigger function could then fetch the user’s profile from the database.&lt;/p&gt;

&lt;p&gt;This setup might be easier to manage in a rapidly changing environment, though it adds an extra API call to the database, which could slow down the response time.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Summary
&lt;/h2&gt;

&lt;p&gt;Combining a pre-token generation trigger with a custom API Gateway authorizer function allows for more sophisticated authorization flows.&lt;/p&gt;

&lt;p&gt;The pre-token generation function adds custom details to the ID and access tokens. Then, a Lambda authorizer can enforce the custom authorization logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Further reading
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/getting-started-with-cognito-user-pools.html" rel="noopener noreferrer"&gt;Getting started with user pools&lt;/a&gt; - How to create a user pool with an app client&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-user-groups.html" rel="noopener noreferrer"&gt;Adding groups to a user pool&lt;/a&gt; - The title says it all&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.html" rel="noopener noreferrer"&gt;Pre token generation Lambda trigger&lt;/a&gt; - Detailed reference for adding a trigger&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-client-apps.html#cognito-user-pools-app-idp-settings-console-create" rel="noopener noreferrer"&gt;Creating an app client&lt;/a&gt; - How to create an app client for a user pool&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started.html" rel="noopener noreferrer"&gt;Get started with API Gateway&lt;/a&gt; - How to create an API Gateway resource&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/getting-started.html" rel="noopener noreferrer"&gt;Create your first function&lt;/a&gt; - AWS Lambda "Getting started"&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>lambda</category>
      <category>apigateway</category>
    </item>
    <item>
      <title>Manual Cognito user registration approvals with Step Functions</title>
      <dc:creator>Arpad Toth</dc:creator>
      <pubDate>Thu, 13 Mar 2025 10:12:56 +0000</pubDate>
      <link>https://dev.to/aws-builders/manual-user-registration-approvals-in-multi-tenant-applications-with-step-functions-12ae</link>
      <guid>https://dev.to/aws-builders/manual-user-registration-approvals-in-multi-tenant-applications-with-step-functions-12ae</guid>
      <description>&lt;p&gt;New user registration for an application that serves multiple users from the same organization might need manual approval. We can use Step Functions to build a serverless workflow for user confirmation.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Problem statement
&lt;/h2&gt;

&lt;p&gt;Let’s say we have launched &lt;code&gt;Awesome Application&lt;/code&gt;, a tool that stores documents for companies that subscribe to it. Multiple users from the same company can sign up and access shared company documents.&lt;/p&gt;

&lt;p&gt;We’re using a &lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/getting-started-user-pools.html" rel="noopener noreferrer"&gt;Cognito user pool&lt;/a&gt; to manage users and issue tokens. The &lt;strong&gt;ID&lt;/strong&gt; and &lt;strong&gt;access&lt;/strong&gt; tokens from the user pool work with &lt;a href="https://aws.amazon.com/api-gateway/" rel="noopener noreferrer"&gt;API Gateway&lt;/a&gt;, the entry point to &lt;code&gt;Awesome Application&lt;/code&gt;, to authorize requests to endpoints.&lt;/p&gt;

&lt;p&gt;Self-sign-up is enabled in Cognito. When new users register for &lt;code&gt;Awesome Application&lt;/code&gt;, they provide:&lt;/p&gt;

&lt;ul&gt;
    &lt;li&gt;Their &lt;strong&gt;email&lt;/strong&gt; address, which doubles as their username for logging in later.&lt;/li&gt;
    &lt;li&gt;A strong password.&lt;/li&gt;
    &lt;li&gt;The &lt;strong&gt;name of the company&lt;/strong&gt; they work for.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After signing up, users log in with their &lt;strong&gt;email address&lt;/strong&gt; and &lt;strong&gt;password&lt;/strong&gt;. The company name is not used in user login.&lt;/p&gt;

&lt;p&gt;But there is an issue.&lt;/p&gt;

&lt;p&gt;With an automated sign-up process, users could enter &lt;strong&gt;any&lt;/strong&gt; company name and pretend to be an employee of that company. This would give them access to sensitive documents from any company they choose.&lt;/p&gt;

&lt;p&gt;To address this, we set these rules:&lt;/p&gt;

&lt;ul&gt;
    &lt;li&gt;Every user must be &lt;strong&gt;manually approved&lt;/strong&gt; by an admin from their company. The admin verifies that the user works there.&lt;/li&gt;
    &lt;li&gt;There’s &lt;strong&gt;no automatic sign-up&lt;/strong&gt; allowed.&lt;/li&gt;
    &lt;li&gt;Users must be added to their company’s &lt;strong&gt;Cognito group&lt;/strong&gt;. When they log in, the tokens issued by the user pool will include the company name for authorization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these requirements in mind, let’s explore a possible solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pre-requisites
&lt;/h2&gt;

&lt;p&gt;This post will not cover the following:&lt;/p&gt;

&lt;ul&gt;
    &lt;li&gt;Setting up a user pool with an app client and Lambda triggers in Cognito.&lt;/li&gt;
    &lt;li&gt;Creating IAM roles.&lt;/li&gt;
    &lt;li&gt;Building Lambda functions and configuring API Gateway.&lt;/li&gt;
    &lt;li&gt;Setting up SES to send emails.&lt;/li&gt;
    &lt;li&gt;Creating a DynamoDB table and adding data.&lt;/li&gt;
    &lt;li&gt;Designing a Step Functions state machine.&lt;/li&gt;
    &lt;li&gt;Writing front-end code with React and Amplify.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will include links to relevant documentation below for anyone who needs them.&lt;/p&gt;

&lt;p&gt;Also, the solution here &lt;strong&gt;is not ready for production&lt;/strong&gt;. It’s meant for learning purposes, showing an idea and some patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Architecture
&lt;/h2&gt;

&lt;p&gt;This architecture focuses on the sign-up workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhxhi09c9qu0bpvjzvre8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhxhi09c9qu0bpvjzvre8.png" alt="Sign-up workflow with manual approval" width="800" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We use a &lt;strong&gt;single&lt;/strong&gt; Cognito user pool to store users and their companies. Once a company admin approves a user registration, the workflow adds the user to the right &lt;strong&gt;Cognito group&lt;/strong&gt;. If the admin rejects the request or the company name is invalid, the user's directory status stays unconfirmed. Cognito blocks unconfirmed users from logging into the application.&lt;/p&gt;

&lt;p&gt;The core of this setup is a &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html" rel="noopener noreferrer"&gt;Step Functions&lt;/a&gt; &lt;strong&gt;standard&lt;/strong&gt; state machine that handles the manual approval step.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Digging deep
&lt;/h2&gt;

&lt;p&gt;Let’s break down each part of the process.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1. User registration
&lt;/h3&gt;

&lt;p&gt;Since self-sign-up is enabled, users can go to the &lt;strong&gt;Sign up&lt;/strong&gt; page in &lt;code&gt;Awesome Application&lt;/code&gt; and fill out the &lt;strong&gt;Create account&lt;/strong&gt; form. They enter their &lt;strong&gt;email&lt;/strong&gt; address, a &lt;strong&gt;password&lt;/strong&gt;, and the &lt;strong&gt;name of the company&lt;/strong&gt; they work for. To use the email as the username, we mark &lt;code&gt;email&lt;/code&gt; as a &lt;strong&gt;Required attribute&lt;/strong&gt; in the user pool.&lt;/p&gt;

&lt;p&gt;If we want the company name in the tokens, we need a &lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-custom-attributes" rel="noopener noreferrer"&gt;custom attribute&lt;/a&gt; in the user pool. Let’s call it &lt;code&gt;company_name&lt;/code&gt;. This means the token will include a property called &lt;code&gt;custom:company_name&lt;/code&gt; with the relevant value.&lt;/p&gt;

&lt;p&gt;We also &lt;strong&gt;turn off&lt;/strong&gt; Cognito’s automatic verification emails. With this feature enabled, Cognito sends new users an email to confirm their address, but we are handling approvals manually instead.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpg85cbj2brjpvd438cg8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpg85cbj2brjpvd438cg8.png" alt="User pool sign-up settings" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, Cognito does not allow us to make &lt;code&gt;company_name&lt;/code&gt; a required attribute. We will need a workaround, like the one in this post, to handle that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvy7gevuxaah2obe5os7e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvy7gevuxaah2obe5os7e.png" alt="Email sign-in option" width="800" height="120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We set &lt;code&gt;Email&lt;/code&gt; as the &lt;strong&gt;Cognito user pool sign-in option&lt;/strong&gt; since users log in with their email addresses. Note that sign-in options and required attributes can’t be changed after we have created the user pool.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2. Using Amplify
&lt;/h3&gt;

&lt;p&gt;Amplify’s built-in UI and login features don’t support our custom &lt;code&gt;company_name&lt;/code&gt; field in the sign-up form, so we have to build the form ourselves.&lt;/p&gt;

&lt;p&gt;That said, we can still use Amplify. It offers methods for sign-up, sign-in, and sign-out that handle Cognito API calls behind the scenes.&lt;/p&gt;

&lt;p&gt;Here is part of a possible &lt;code&gt;SignUp&lt;/code&gt; &lt;a href="https://react.dev/" rel="noopener noreferrer"&gt;React&lt;/a&gt; component’s &lt;code&gt;handleSignUp&lt;/code&gt; function, triggered when the user submits the form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signUp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-amplify/auth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Other imports&lt;/span&gt;

&lt;span class="c1"&gt;// SignUp React component&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SignUp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ... set states here&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSignUp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Omitted for brevity&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;signUp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Use email as the username&lt;/span&gt;
      &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;userAttributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;custom:company_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;companyName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Our custom attribute&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Create the form here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;SignUp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are similar methods for sign-in and sign-out flows.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3. Pre-sign-up Lambda trigger
&lt;/h3&gt;

&lt;p&gt;When the user submits the form, Amplify’s &lt;code&gt;signUp&lt;/code&gt; method adds them to the Cognito user pool with an &lt;code&gt;Unconfirmed&lt;/code&gt; status.&lt;/p&gt;

&lt;p&gt;At this point, Cognito can trigger a pre-sign-up Lambda function. We use it to start the Step Functions workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SFNClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StartExecutionCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/client-sfn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sfnClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SFNClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;STATE_MACHINE_ARN&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// This is the sub from the user pool&lt;/span&gt;
    &lt;span class="nx"&gt;userPoolId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;userAttributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;custom:company_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;companyName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;email&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stateMachineInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;companyName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;userPoolId&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startExecutionCommand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StartExecutionCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;stateMachineArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;STATE_MACHINE_ARN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stateMachineInput&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`NewUserRegistration-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Start the state machine execution asynchronously&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sfnClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;startExecutionCommand&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Return the event object to allow sign-up to proceed&lt;/span&gt;
    &lt;span class="c1"&gt;// The user stays unconfirmed until approved&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;event&lt;/code&gt; object has everything we need to start the state machine and looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eu-central-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"userPoolId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USER_POOL_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"userName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"029f515c-97db-4e3f-8905-f7b5b82db993"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// Cognito user name (sub)&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"triggerSource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PreSignUp_SignUp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"userAttributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"custom:company_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MyCompany"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"john.doe@mycompany.com"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="c1"&gt;// ...other properties here&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4.4. The state machine
&lt;/h3&gt;

&lt;p&gt;The state machine uses &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/transforming-data.html" rel="noopener noreferrer"&gt;JSONata&lt;/a&gt; syntax to manage variables and state inputs/outputs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6y2gv6s9eftm4mt0kpcc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6y2gv6s9eftm4mt0kpcc.png" alt="Manual approval state machine" width="800" height="762"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;StoreVariables&lt;/code&gt; state sets up variables that are available for each state in the workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{% $states.input.userName %}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"companyName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{% $states.input.companyName %}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userPoolId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{% $states.input.userPoolId %}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userEmail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{% $states.input.email %}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps us from passing these variables through every state, unlike with JSONPath.&lt;/p&gt;

&lt;p&gt;Here are the main workflow steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.4.a&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;GetAdminsForCompany&lt;/code&gt; Lambda function queries a DynamoDB table that lists admins who can approve or deny registration requests for a company. A company might have multiple admins. In this case, all of them get the approval email.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;HasAdmin&lt;/code&gt; &lt;strong&gt;Choice&lt;/strong&gt; state checks if there are admin emails for the company provided.&lt;/p&gt;

&lt;p&gt;If there are none, say, because the user entered an invalid company name, the workflow &lt;strong&gt;deletes the user&lt;/strong&gt; from the user pool using the &lt;code&gt;AdminDeleteUser&lt;/code&gt; Cognito API. It also &lt;strong&gt;sends&lt;/strong&gt; the user an email via &lt;a href="https://docs.aws.amazon.com/ses/latest/dg/Welcome.html" rel="noopener noreferrer"&gt;SES&lt;/a&gt; saying the registration failed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.4.b&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;GetAdminsForCompany&lt;/code&gt; finds admin emails, Step Functions calls the &lt;code&gt;SendApprovalEmail&lt;/code&gt; function. Here is what that might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SESClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SendEmailCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/client-ses&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sesClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SESClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;API_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FROM_EMAIL&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;userPoolId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;userEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;adminEmails&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;executionContext&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;taskToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;executionContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Create approve/deny URLs&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;approveUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/confirm-registration?action=approve&amp;amp;token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskToken&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;denyUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/confirm-registration?action=deny&amp;amp;token=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskToken&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
    New User Registration Requires Approval

    User Details:
    Username: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
    User Pool ID: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userPoolId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
    Email: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;

    Actions:
    To approve: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;approveUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
    To deny: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;denyUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Send email using SES&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailParams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;ToAddresses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adminEmails&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;Charset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UTF-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;textBody&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Charset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UTF-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`New User Registration - &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FROM_EMAIL&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendEmailCommand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SendEmailCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailParams&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sesClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sendEmailCommand&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error sending approval email:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function’s &lt;code&gt;event&lt;/code&gt; input looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c1"&gt;// From the previous state&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"adminEmails"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{% $states.input.adminEmails %}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c1"&gt;// Variables from StoreVariables&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{% $userName %}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userPoolId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{% $userPoolId %}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userEmail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{% $userEmail %}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c1"&gt;// From the state’s context&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"executionContext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{% $states.context %}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This state handles the manual approval. We use Step Functions’ &lt;strong&gt;Wait for callback&lt;/strong&gt; feature in this &lt;strong&gt;Task&lt;/strong&gt; state, which adds a task token to the &lt;strong&gt;execution context&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The function builds &lt;code&gt;approve&lt;/code&gt; and &lt;code&gt;deny&lt;/code&gt; URLs with the task token, then sends an email to the admins via SES with those links.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4.4.c&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The URLs point to an API Gateway endpoint backed by a &lt;strong&gt;Lambda function&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;SendApprovalEmail&lt;/code&gt; function included the &lt;strong&gt;task token&lt;/strong&gt; in the URLs as a query parameter. The backend Lambda extracts it - along with the &lt;code&gt;action&lt;/code&gt; parameter (approve or deny) - from the API Gateway &lt;code&gt;event&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queryStringParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;taskToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queryStringParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approve&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Approved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Denied&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;taskToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sfnClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SendTaskSuccessCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Success!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a key step: the function calls Step Functions’ &lt;code&gt;SendTaskSuccess&lt;/code&gt; API with the &lt;strong&gt;task token&lt;/strong&gt; and an &lt;code&gt;output&lt;/code&gt; field. That &lt;code&gt;output&lt;/code&gt; becomes the input for the next state - here, it’s just &lt;code&gt;Approved&lt;/code&gt; or &lt;code&gt;Denied&lt;/code&gt;. When Step Functions gets the task token, the workflow execution will resume.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.5. Manual approval
&lt;/h3&gt;

&lt;p&gt;The admin approves or denies the request by clicking the appropriate link in the email.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.6. Approved or denied?
&lt;/h3&gt;

&lt;p&gt;If the admin &lt;strong&gt;approves&lt;/strong&gt;, Step Functions calls the &lt;code&gt;AdminConfirmSignUp&lt;/code&gt; Cognito API to confirm the user, changing their status to &lt;code&gt;Confirmed&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the admin &lt;strong&gt;denies&lt;/strong&gt;, this example uses a simple &lt;strong&gt;Pass&lt;/strong&gt; state. You could replace it with a state to delete the user with &lt;code&gt;AdminDeleteUser&lt;/code&gt;, as we did earlier, and notify them.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.7. Post-confirmation Lambda trigger
&lt;/h3&gt;

&lt;p&gt;Once the user is &lt;code&gt;Confirmed&lt;/code&gt;, Cognito can trigger a &lt;strong&gt;post-confirmation Lambda&lt;/strong&gt; function. This adds the user to their company’s &lt;strong&gt;group&lt;/strong&gt; in the user pool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CognitoIdentityProviderClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AdminAddUserToGroupCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/client-cognito-identity-provider&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cognitoClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CognitoIdentityProviderClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userPoolId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userPoolId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;companyName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userAttributes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;custom:company_name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AdminAddUserToGroupCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;UserPoolId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userPoolId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;GroupName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;companyName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cognitoClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error adding user to group:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Retry or send a notification&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it! Next time the user logs in with their email and password, their tokens will include &lt;code&gt;custom:company_name&lt;/code&gt;, ready for authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Considerations
&lt;/h2&gt;

&lt;p&gt;This solution doesn’t cover every detail of a production-ready system. Here are some things to keep in mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.1. Permissions
&lt;/h3&gt;

&lt;p&gt;Ensure the Step Functions state machine and Lambda functions have the right permissions for Cognito, DynamoDB, and SES.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.2. Admin items and groups
&lt;/h3&gt;

&lt;p&gt;This solution assumes admin emails and company groups already exist in DynamoDB and Cognito. Setting those up could be a separate process, maybe with API Gateway and Lambda endpoints for admins to add them before users sign up.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.3. Error handling and retries
&lt;/h3&gt;

&lt;p&gt;To keep things concise, I skipped error handling. In practice, you would need to add error checks and retries in the code, use Step Functions for retries, or send alerts via &lt;a href="https://docs.aws.amazon.com/sns/latest/dg/welcome.html" rel="noopener noreferrer"&gt;SNS&lt;/a&gt;, depending on your needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.4. Company name spelling
&lt;/h3&gt;

&lt;p&gt;Users might &lt;strong&gt;misspell&lt;/strong&gt; their company name during sign-up. If so, the workflow rejects them because the input won’t match the corresponding items in the database.&lt;/p&gt;

&lt;p&gt;You could leave it as is or add something like a type-ahead search to help.&lt;/p&gt;

&lt;h3&gt;
  
  
  5.5. Manual addition
&lt;/h3&gt;

&lt;p&gt;Alternatively, admins could manually add users to Cognito and their company group via admin endpoints. Users would get a username and temporary password, which they would change on the first login. Tokens would still include company info, but you would skip the Step Functions workflow entirely.&lt;/p&gt;

&lt;p&gt;This simpler approach cuts automation but might make sense if time is tight or explaining Step Functions to new developers feels tricky. I would still go with the workflow, though.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Summary
&lt;/h2&gt;

&lt;p&gt;When users from the same company need access to shared documents in our app, automatic sign-ups might not be the best fit.&lt;/p&gt;

&lt;p&gt;Instead, we can use Step Functions and Cognito Lambda triggers to build a workflow that includes manual approval for new users.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Further reading
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/getting-started-with-cognito-user-pools.html" rel="noopener noreferrer"&gt;Getting started with user pools&lt;/a&gt; - How to create a user pool with an app client&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create.html" rel="noopener noreferrer"&gt;IAM role creation&lt;/a&gt; - How to create IAM roles&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/getting-started.html" rel="noopener noreferrer"&gt;Create your first Lambda function&lt;/a&gt; - How to create Lambda functions&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/ses/latest/dg/setting-up.html#quick-start-verify-email-addresses" rel="noopener noreferrer"&gt;Set up your SES account&lt;/a&gt; - Using the SES Wizard to quickly set up the account&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/ses/latest/dg/send-email.html" rel="noopener noreferrer"&gt;Set up email sending with Amazon SES&lt;/a&gt; - Different ways of sending emails with SES&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStartedDynamoDB.html" rel="noopener noreferrer"&gt;Getting started with DynamoDB&lt;/a&gt; - DynamoDB basics&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/getting-started.html" rel="noopener noreferrer"&gt;Learn how to get started with Step Functions&lt;/a&gt; - Step Functions tutorial&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.amplify.aws/gen1/react/build-a-backend/auth/" rel="noopener noreferrer"&gt;Authentication&lt;/a&gt; - Use Amplify with Cognito for authentication&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>security</category>
      <category>stepfunctions</category>
    </item>
  </channel>
</rss>
