Build a Serverless Checkout with CI/CD

Build a secure AWS checkout with Stripe webhooks, SQS, and GitHub Actions.

Introduction

30 Second Summary

A checkout can show a successful confirmation page while the order behind it never gets marked as paid. That gap leaves customers waiting for purchases the business thinks are complete.

In this project, you will build a secure checkout backend for a $15 Cloud Mug using verified Stripe sandbox payments. You will ship it to AWS through keyless CI/CD from GitHub.

What You'll Build

You'll complete a sandbox payment to watch the matching order move from PENDING to PAID through asynchronous fulfillment.

By the end of this project, you'll have:

  • A public catalog response showing the Cloud Mug with unitAmount set to 1500.
  • A Stripe sandbox checkout that turns a completed test payment into a durable PAID order.
  • A keyless push-to-deploy workflow that turns each main branch update into a tested AWS SAM deployment.
  • Secret Mission: Make the worker reject duplicate payment delivery with an atomic update that keeps fulfillmentCount at 1.

Are there any prerequisites?

You need a Windows device with an existing AWS Free Plan account. The project guides you through every remaining tool and account.

Before We Start

The tools come later. First, connect every technical choice to the secure checkout backend you are about to build.

Reliable payment fulfillment means each successful payment produces one durable order update. This keeps the customer's payment state accurate when events are delayed or delivered more than once.

Set Up Your Windows Toolchain

A secure checkout depends on a complete local toolchain before it can reach the cloud. One missing command would stop testing or deployment before your backend exists.

You will prepare PowerShell with Node.js 24 LTS. You will also install Visual Studio Code plus Git for Windows.

Your deployment tools are AWS CLI v2 plus AWS SAM CLI 1.166.2. You will also prepare GitHub plus a Stripe sandbox.

In this step, get ready to:
  • Install the Windows development toolchain.
  • Prepare an empty GitHub repository plus a Stripe sandbox account.
  • Verify the complete toolchain plus a temporary AWS session.
Install the Windows tools

Version checks reveal which tools Windows can already find. A fresh machine may report missing commands during this first check.

  • Check the current toolchain by running these commands in PowerShell:
node --version; npm --version; git --version; code --version; aws --version; sam --version

What does this check do?

  • The first two commands check Node.js plus the bundled npm command.
  • The next two commands check Git plus the Visual Studio Code command-line launcher.
  • The final two commands check the AWS deployment tools.

Why versions matter

This project targets the Node.js 24 runtime used by the deployed Lambda functions. Matching that major version reduces differences between local tests and AWS.

AWS browser login requires AWS CLI 2.32.0 or later. The deployment workflow also installs AWS SAM CLI 1.166.2.

✔️ Every command shows a version

All six commands are available. The required releases are Node.js v24, AWS CLI 2.32.0 or later, plus AWS SAM CLI 1.166.2.

ⓧ I see an older required version

An older release can produce local behavior that differs from the deployed application. Update each pinned tool before continuing.

The installer confirms when Node.js plus npm are ready.

  • Update AWS CLI v2 for your Windows user by running this command:
irm https://awscli.amazonaws.com/v2/install.ps1 | iex

What does this command do?

The official AWS installation script downloads AWS CLI v2 for the current Windows user. This gives PowerShell access to AWS commands without creating permanent access keys.

The installer confirms when AWS SAM CLI is available.

  • Restart PowerShell from Windows Search.
  • Verify the updated toolchain by running:
node --version; npm --version; git --version; code --version; aws --version; sam --version

How should the versions look?

The output should now identify Node.js v24, AWS CLI 2.32.0 or later, plus AWS SAM CLI 1.166.2.

ⓧ One or more commands are missing

Missing commands confirm that the toolchain still needs to be installed. Work through each installer before restarting PowerShell.

The installer confirms when Node.js plus npm are ready.

The completed setup adds the Visual Studio Code launcher to future PowerShell sessions.

The completed installer makes Git available to new PowerShell sessions.

  • Install AWS CLI v2 for your Windows user by running this command:
irm https://awscli.amazonaws.com/v2/install.ps1 | iex

What does this command do?

The official AWS installation script installs AWS CLI v2 for the current Windows user. A later browser sign-in supplies temporary credentials.

The completed installer adds the SAM command to future PowerShell sessions.

  • Restart PowerShell from Windows Search.
  • Verify every installed tool by running:
node --version; npm --version; git --version; code --version; aws --version; sam --version

What should this prove?

Each command should now print version information without reporting a missing executable. Node.js should use major version 24. AWS SAM CLI should report 1.166.2.

Still missing a command?

  • Restart Windows if a new PowerShell session still uses the old command search path.
  • Rerun the installer for the specific tool that remains unavailable.
  • Confirm that you downloaded the Windows installer for your device.

Still stuck? Help me find why an installed tool is missing from PowerShell.

Prepare GitHub and Stripe

GitHub stores the source repository that the deployment workflow will use. Stripe supplies a sandbox where checkout payments use test values without moving funds.

Your GitHub personal account is ready when the verified profile opens successfully.

  • Select New repository from the GitHub home page.
  • Enter secure-checkout as the repository name.

The repository form should now show secure-checkout as its name.

  • Leave the README, license, plus .gitignore initialization options unselected.
  • Click Create repository.

You should see an empty secure-checkout repository with instructions for adding local code.

  • Visit Stripe's website in your browser.
  • Create a Stripe account using your normal sign-in details.

Your Stripe dashboard confirms that the account is ready.

  • Open the Stripe sandbox to confirm that test data is available.

Why use the sandbox?

Stripe sandbox transactions use test values without moving funds. This lets you exercise the complete payment flow safely.

Sign in to AWS and verify the setup

AWS CLI can use a browser sign-in to obtain temporary credentials. This avoids storing long-lived AWS access keys on your machine.

Keep the Free Plan intact

Use your existing AWS Free Plan account for this project. Creating or joining an AWS Organization upgrades the account to a paid plan.

That upgrade also ends the account's remaining Free Tier credits. Leave AWS Organizations unchanged throughout this project.

  • Press the Windows key to open Windows Search.
  • Type PowerShell into the search field.
  • Press Enter to start a new PowerShell session.

The next command opens a browser sign-in flow for your AWS account. PowerShell receives temporary credentials after the browser confirms your identity.

  • Start the AWS browser sign-in by running this command:
aws login

What does this command do?

AWS CLI opens the browser so you can authenticate with your existing account. It then stores a temporary session for later AWS commands.

  • Complete the browser sign-in with your existing AWS Free Plan account.
  • Return to PowerShell after the sign-in completes.

Before you check the caller identity, do you expect AWS to recognize the signed-in account?

  • Check the temporary AWS identity by running:
aws sts get-caller-identity

What does this check prove?

The identity request asks AWS which account is making the call. A returned identity confirms that the browser session supplied valid temporary credentials.

You should see a response that identifies your AWS account plus the temporary caller.

Before the final check, which result would reveal that one installer has not reached this new PowerShell session?

  • Verify the complete toolchain plus the AWS session by running:
node --version; npm --version; git --version; code --version; aws --version; sam --version; aws sts get-caller-identity

What should the final check show?

The command prints six tool versions before requesting your AWS identity. Every command should complete without a missing executable message.

That is the foundation locked in: Windows can now run every command required for the checkout.

Does the final check fail?

  • Restart Windows if an installed command remains unavailable after reopening PowerShell.
  • Run the browser login again if the temporary AWS session is unavailable.
  • Confirm that the AWS identity belongs to the Free Plan account you intend to use.

Need another pair of eyes? Help me troubleshoot my final Windows toolchain verification.

Your accounts plus Windows tools are ready without any local project files or AWS application resources. Next, you will build the service bundle and deploy its first catalog endpoint.

Deploy the Checkout Services

The checkout needs a working cloud foundation before payments can flow through it. This step proves that your local code can reach a public endpoint on AWS.

You will package four focused AWS Lambda services with AWS SAM. Your first visible result is a deployed catalog response containing the Cloud Mug and its trusted server-side price.

Why Start With the Catalog?

The catalog has no payment flow to coordinate. A successful response proves that your template deployed correctly.

It also proves that the public API can invoke your Lambda code. That gives the checkout flow a tested foundation.

In this step, get ready to:
  • Create the Node.js service bundle and confirm its product tests pass.
  • Build and deploy the serverless infrastructure with your temporary AWS credentials.
  • Call the deployed catalog endpoint and inspect the Cloud Mug response.
Create and test the service bundle

Each handler owns one part of the checkout system. The shared product definition keeps the Cloud Mug price on the server.

  • Create the secure-checkout folder on your Desktop by running these commands in PowerShell:
Set-Location -Path "$env:USERPROFILE\Desktop"
New-Item -Path "$env:USERPROFILE\Desktop" -Name "secure-checkout" -ItemType "Directory"
Set-Location -Path "$env:USERPROFILE\Desktop\secure-checkout" -PassThru

What Do These Commands Do?

PowerShell moves to your Desktop before creating the secure-checkout folder. The final command enters the folder and prints its path.

You should see a path ending in secure-checkout. This confirms that every project file has a predictable home.

  • Open the current secure-checkout folder in Visual Studio Code by running this command:
code .

What Does This Command Do?

The code command loads the current folder into Visual Studio Code. The dot represents your current secure-checkout location.

  • Create the src folder in the Visual Studio Code Explorer sidebar.
  • Create the scripts folder in the secure-checkout folder.
  • Create the test folder in the secure-checkout folder.
  • Create each file shown in the full-code tab at its displayed path.
  • Copy the displayed contents into each matching file.
  • Save every project file.

✔️ Awesome, I've got everything!

Check that Visual Studio Code shows package.json and template.yaml inside secure-checkout.

The src folder should contain five modules. The scripts folder and test folder should each contain one file.

ⓧ I'd like to double check the full code

Check .gitignore
node_modules/
.aws-sam/
.env
*.log

What Does This File Do?

These rules exclude installed packages and generated AWS SAM files from version control. They also exclude local environment files and logs.

Check package.json
{
  "name": "secure-serverless-checkout",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "engines": {
    "node": ">=24 <25"
  },
  "scripts": {
    "test": "node --test"
  },
  "dependencies": {
    "@aws-sdk/client-dynamodb": "3.1136.0",
    "@aws-sdk/client-sqs": "3.1136.0",
    "@aws-sdk/lib-dynamodb": "3.1136.0",
    "stripe": "22.6.2"
  }
}

What Does This File Do?

This file defines a private Node.js 24 ES module project. It pins the AWS SDK clients and Stripe package used by the Lambda handlers.

The test script starts the built-in Node.js test runner.

Check src/product.mjs
export const PRODUCT = Object.freeze({
  id: "cloud-mug",
  name: "Cloud Mug",
  description: "A demo mug for a secure serverless checkout.",
  unitAmount: 1500,
  currency: "usd"
});

What Does This File Do?

The PRODUCT object holds the trusted catalog entry. Freezing it protects the identifier and price from accidental runtime changes.

Check src/http.mjs
export function json(statusCode, body) {
  return {
    statusCode,
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body)
  };
}

What Does This File Do?

The json() helper gives every public handler the same response structure. It serializes the response body as JSON.

Check src/catalog.mjs
import { PRODUCT } from "./product.mjs";
import { json } from "./http.mjs";

export const handler = async () => json(200, PRODUCT);

What Does This File Do?

The catalog handler returns the fixed PRODUCT with a successful HTTP status. This becomes the first deployed endpoint you test.

Check src/checkout.mjs
import { randomUUID } from "node:crypto";
import Stripe from "stripe";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocument } from "@aws-sdk/lib-dynamodb";
import { PRODUCT } from "./product.mjs";
import { json } from "./http.mjs";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const orders = DynamoDBDocument.from(new DynamoDBClient({}));

export const handler = async (event) => {
  const method = event.requestContext?.http?.method;

  if (method === "GET") {
    const orderId = event.pathParameters?.orderId;
    const result = await orders.get({
      TableName: process.env.ORDERS_TABLE,
      Key: { orderId }
    });

    return result.Item
      ? json(200, result.Item)
      : json(404, { message: "Order not found" });
  }

  const request = JSON.parse(event.body || "{}");
  const quantity = Number(request.quantity || 1);

  if (!Number.isInteger(quantity) || quantity < 1 || quantity > 5) {
    return json(400, { message: "quantity must be an integer from 1 to 5" });
  }

  const orderId = randomUUID();
  const session = await stripe.checkout.sessions.create({
    mode: "payment",
    line_items: [
      {
        quantity,
        price_data: {
          currency: PRODUCT.currency,
          unit_amount: PRODUCT.unitAmount,
          product_data: {
            name: PRODUCT.name,
            description: PRODUCT.description
          }
        }
      }
    ],
    success_url: "https://example.com/success",
    metadata: { orderId }
  });

  const order = {
    orderId,
    sessionId: session.id,
    productId: PRODUCT.id,
    quantity,
    amount: PRODUCT.unitAmount * quantity,
    currency: PRODUCT.currency,
    status: "PENDING",
    fulfillmentCount: 0,
    createdAt: new Date().toISOString()
  };

  await orders.put({
    TableName: process.env.ORDERS_TABLE,
    Item: order
  });

  return json(201, {
    orderId,
    status: order.status,
    checkoutUrl: session.url
  });
};

What Does This File Do?

The checkout handler retrieves existing orders for GET requests. For POST requests it validates the quantity before creating a Stripe Checkout Session.

It calculates the amount from PRODUCT.unitAmount. It stores each new order with the status PENDING.

Check src/webhook.mjs
import Stripe from "stripe";
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
import { json } from "./http.mjs";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const sqs = new SQSClient({});

export const handler = async (event) => {
  const rawBody = event.isBase64Encoded
    ? Buffer.from(event.body, "base64").toString("utf8")
    : event.body;
  const signature = event.headers?.["stripe-signature"];

  let stripeEvent;
  try {
    stripeEvent = stripe.webhooks.constructEvent(
      rawBody,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (error) {
    console.error("Webhook signature verification failed", error.message);
    return json(400, { message: "Invalid webhook signature" });
  }

  if (stripeEvent.type === "checkout.session.completed") {
    const session = stripeEvent.data.object;
    await sqs.send(
      new SendMessageCommand({
        QueueUrl: process.env.PAYMENT_QUEUE_URL,
        MessageBody: JSON.stringify({
          eventId: stripeEvent.id,
          orderId: session.metadata.orderId,
          sessionId: session.id
        })
      })
    );
  }

  return json(200, { received: true });
};

What Does This File Do?

The webhook handler preserves the raw request body for Stripe signature verification. A verified completed Checkout event becomes an SQS message.

An invalid signature receives an HTTP error response before it can reach fulfillment.

Check src/fulfillment.mjs
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, UpdateCommand } from "@aws-sdk/lib-dynamodb";

const orders = DynamoDBDocumentClient.from(new DynamoDBClient({}));

export const handler = async (event) => {
  for (const record of event.Records) {
    const payment = JSON.parse(record.body);

    await orders.send(
      new UpdateCommand({
        TableName: process.env.ORDERS_TABLE,
        Key: { orderId: payment.orderId },
        UpdateExpression:
          "SET #status = :paid, paymentEventId = :eventId, paidAt = :paidAt ADD fulfillmentCount :one",
        ExpressionAttributeNames: {
          "#status": "status"
        },
        ExpressionAttributeValues: {
          ":paid": "PAID",
          ":eventId": payment.eventId,
          ":paidAt": new Date().toISOString(),
          ":one": 1
        }
      })
    );
  }
};

What Does This File Do?

The fulfillment handler processes every queued payment message. Its DynamoDB update changes the order status to PAID.

The same update increments fulfillmentCount to record the completed fulfillment.

Check scripts/register-webhook.mjs
import Stripe from "stripe";

const apiUrl = process.argv[2];
if (!apiUrl || !process.env.STRIPE_SECRET_KEY) {
  throw new Error("Provide the API URL and set STRIPE_SECRET_KEY");
}

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const endpoint = await stripe.webhookEndpoints.create({
  enabled_events: ["checkout.session.completed"],
  url: `${apiUrl}/webhook`,
  description: "Secure Checkout sandbox webhook"
});

console.log(JSON.stringify({ id: endpoint.id, secret: endpoint.secret }));

What Does This File Do?

This utility can register the deployed webhook URL with the Stripe sandbox. It prints the endpoint identifier and signing secret returned during creation.

Check test/product.test.mjs
import assert from "node:assert";
import test from "node:test";
import { PRODUCT } from "../src/product.mjs";

test("the catalog exposes a fixed server-side price", () => {
  assert.strictEqual(PRODUCT.unitAmount, 1500);
  assert.strictEqual(PRODUCT.currency, "usd");
});

test("the product has a stable identifier", () => {
  assert.strictEqual(PRODUCT.id, "cloud-mug");
});

What Does This File Do?

These tests protect the catalog price and currency. They also protect the stable product identifier used by checkout.

Check template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Secure serverless checkout with asynchronous fulfillment

Parameters:
  StripeSecretKey:
    Type: String
    NoEcho: true
  StripeWebhookSecret:
    Type: String
    NoEcho: true
    Default: whsec_not_configured

Globals:
  Function:
    Runtime: nodejs24.x
    CodeUri: .
    Architectures:
      - arm64
    MemorySize: 256
    Timeout: 10

Resources:
  CheckoutApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      StageName: "$default"

  OrdersTable:
    Type: AWS::DynamoDB::Table
    Properties:
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: orderId
          AttributeType: S
      KeySchema:
        - AttributeName: orderId
          KeyType: HASH

  PaymentDeadLetterQueue:
    Type: AWS::SQS::Queue

  PaymentQueue:
    Type: AWS::SQS::Queue
    Properties:
      VisibilityTimeout: 60
      RedrivePolicy:
        deadLetterTargetArn: !GetAtt PaymentDeadLetterQueue.Arn
        maxReceiveCount: 3

  CatalogFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/catalog.handler
      Events:
        CatalogRoute:
          Type: HttpApi
          Properties:
            ApiId: !Ref CheckoutApi
            Path: /products/demo
            Method: GET

  CheckoutFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/checkout.handler
      Environment:
        Variables:
          ORDERS_TABLE: !Ref OrdersTable
          STRIPE_SECRET_KEY: !Ref StripeSecretKey
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref OrdersTable
      Events:
        CreateCheckoutRoute:
          Type: HttpApi
          Properties:
            ApiId: !Ref CheckoutApi
            Path: /checkout
            Method: POST
        GetOrderRoute:
          Type: HttpApi
          Properties:
            ApiId: !Ref CheckoutApi
            Path: /orders/{orderId}
            Method: GET

  WebhookFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/webhook.handler
      Environment:
        Variables:
          PAYMENT_QUEUE_URL: !Ref PaymentQueue
          STRIPE_SECRET_KEY: !Ref StripeSecretKey
          STRIPE_WEBHOOK_SECRET: !Ref StripeWebhookSecret
      Policies:
        - SQSSendMessagePolicy:
            QueueName: !GetAtt PaymentQueue.QueueName
      Events:
        WebhookRoute:
          Type: HttpApi
          Properties:
            ApiId: !Ref CheckoutApi
            Path: /webhook
            Method: POST

  FulfillmentFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/fulfillment.handler
      Environment:
        Variables:
          ORDERS_TABLE: !Ref OrdersTable
      Policies:
        - DynamoDBWritePolicy:
            TableName: !Ref OrdersTable
        - SQSPollerPolicy:
            QueueName: !GetAtt PaymentQueue.QueueName
      Events:
        PaymentEvent:
          Type: SQS
          Properties:
            Queue: !GetAtt PaymentQueue.Arn
            BatchSize: 1

Outputs:
  ApiUrl:
    Description: Base URL for the checkout API
    Value: !Sub "https://${CheckoutApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}"

What Does This File Do?

The template defines the HTTP API and four Lambda functions. It also creates the orders table and both payment queues.

Each function receives the DynamoDB or SQS permissions needed for its role. The ApiUrl output provides the deployed base URL.

  • Return to PowerShell in the secure-checkout folder.
  • Install the pinned project dependencies by running this command:
npm install --package-lock=false

What Does This Command Do?

The command installs the dependency versions listed in package.json. The flag prevents npm from creating a package lock file.

The first installation can take a moment while npm downloads the AWS SDK clients and Stripe package. You should see the command finish without a dependency error.

Dependency Installation Failed?

  • Confirm that PowerShell is inside the Desktop secure-checkout folder.
  • Confirm that package.json contains valid JSON.
  • Restart PowerShell if npm is unavailable after the Node.js installation.

Still blocked? Help me diagnose the npm installation failure.

Before you run the tests, do you expect both product rules to pass?

  • Run the product tests by running this command:
npm test

What Does This Command Do?

The npm script starts the Node.js test runner. It loads PRODUCT and checks the fixed catalog values.

You should see two passing tests. That result proves the server-side product contract is ready to deploy.

Tests Not Passing?

  • Compare src/product.mjs with the full-code tab.
  • Confirm that test/product.test.mjs imports ../src/product.mjs.
  • Confirm that package.json sets type to module.

Need another pair of eyes? Help me fix the two Node.js product tests.

That is your local checkpoint complete. Your fixed product price now has two passing tests protecting it.

Build and deploy the AWS stack

The template.yaml file connects an Amazon API Gateway HTTP API to the public handlers. It provisions Amazon DynamoDB for durable order state.

Amazon SQS creates an asynchronous boundary before fulfillment. Its dead-letter queue gives repeated processing failures a separate destination.

Why AWS SAM Here?

AWS SAM keeps the API routes and event sources in one infrastructure template. This suits a serverless system with narrow Lambda responsibilities.

A container platform would add registry and networking setup before you could test the payment path. SAM keeps this project focused on checkout reliability.

Your Stripe sandbox secret grants access to test resources. Keep it in the current PowerShell session so it never enters a project file.

  • Copy your test secret from the Stripe sandbox account you prepared earlier.
  • Replace your-stripe-test-secret-key with that secret in the command below.
  • Set the secret in the current PowerShell session by running the edited command:
$env:STRIPE_SECRET_KEY = "your-stripe-test-secret-key"

What Does This Command Do?

The command stores the Stripe test secret in the current PowerShell process. Closing that window removes the session value.

  • Build the serverless application by running this command:
sam build

What Does This Command Do?

AWS SAM reads template.yaml and prepares the application for deployment. It places generated build output inside .aws-sam.

You should see a successful build result. The output should list the function resources prepared from your template.

SAM Build Failed?

  • Confirm that template.yaml is saved directly inside secure-checkout.
  • Confirm that every handler path matches its file under src.
  • Restart PowerShell if the SAM CLI is unavailable in the current session.

Still blocked? Help me diagnose the AWS SAM build.

The next command creates persistent AWS resources in the secure-checkout CloudFormation stack. You can delete the stack during project cleanup.

The first deployment can take a few minutes while AWS creates the API and service resources. A short pause in the terminal is expected.

  • Deploy the stack to us-east-1 by running this command:
sam deploy --stack-name secure-checkout --resolve-s3 --capabilities CAPABILITY_IAM --region us-east-1 --parameter-overrides "StripeSecretKey=$env:STRIPE_SECRET_KEY" "StripeWebhookSecret=whsec_not_configured" --no-confirm-changeset --no-fail-on-empty-changeset

What Does This Command Do?

  • The stack name groups the API and service resources under secure-checkout.
  • The deployment packages the application for AWS and acknowledges the generated IAM roles.
  • The parameter overrides supply the current Stripe test secret and the webhook placeholder.
  • The final flags support repeat deployments without an interactive change-set confirmation.

You should see the deployment finish with a stack outputs section. That section should include ApiUrl.

Deployment Did Not Complete?

  • Renew your temporary AWS session if the previous browser sign-in expired.
  • Confirm that STRIPE_SECRET_KEY is set in this PowerShell window.
  • Find the first failed resource in the CloudFormation events shown by the deployment.

Need help reading the failure? Help me troubleshoot the secure-checkout deployment.

Call the live catalog endpoint

The ApiUrl output is the public base address for your deployed API. Saving it in API_URL makes later checkout requests shorter.

  • Copy the ApiUrl value from the deployment outputs into your ApiUrl stack output.
  • Set the PowerShell environment variable by running this command:
$env:API_URL = "[[API_URL="your ApiUrl stack output"]]"

What Does This Command Do?

The command stores your deployed API base URL in the current PowerShell session. Later requests can reuse $env:API_URL.

Before you call the endpoint, which Cloud Mug fields do you expect the deployed catalog to return?

  • Call the deployed product route by running this command:
Invoke-RestMethod "$env:API_URL/products/demo"

What Does This Command Do?

PowerShell sends a GET request to /products/demo. API Gateway invokes catalog.handler and returns the serialized product.

You should see the Cloud Mug with unitAmount equal to 1500. You should also see currency equal to usd.

You have your first deployed result. The catalog endpoint now returns its trusted server-side product from AWS.

Catalog Request Failed?

  • Confirm that API_URL contains the ApiUrl value from the completed deployment.
  • Remove any trailing slash from the saved base URL.
  • Confirm that the secure-checkout CloudFormation stack completed successfully.

Still unable to reach the catalog? Help me debug the deployed product endpoint.

Your catalog is live with its fixed server-side price. Next, you will complete a Stripe Checkout payment and expose the missing webhook signal.

Expose the Webhook Gap

Your catalog endpoint is live on AWS. The next goal is to send the Cloud Mug through Stripe Checkout using the deployed checkout service.

A customer-facing redirect can report success while the backend order remains unchanged. This step compares that redirect with the durable record in DynamoDB.

In this step, get ready to:
  • Create a checkout session for one Cloud Mug.
  • Complete the hosted payment with Stripe sandbox data.
  • Query the durable order after payment to expose the missing fulfillment signal.
Create a Checkout session

A Checkout Session gives the browser a hosted payment URL. Your checkout service also writes a durable order before returning that URL.

  • Switch back to the PowerShell window from earlier.
  • Create one Cloud Mug checkout by running this command:
$checkout = Invoke-RestMethod -Method Post -Uri "$env:API_URL/checkout" -ContentType "application/json" -Body '{"quantity":1}'

What does this command do?

  • The Invoke-RestMethod command sends a request to the checkout route in your deployed API.
  • The JSON body requests one Cloud Mug. The backend calculates its fixed server-side price.
  • The $checkout variable keeps the returned order ID plus the hosted checkout URL.
  • Inspect the returned order ID plus checkout URL by running these expressions:
$checkout.orderId
$checkout.checkoutUrl

What are these values?

  • The order ID identifies the durable order that the checkout service saved.
  • The checkout URL points to the hosted payment page for this session.
  • Open the hosted payment page by running this command:
Start-Process $checkout.checkoutUrl

What does this command do?

PowerShell passes the returned checkout URL to your default browser. You should see the hosted page for the Cloud Mug payment.

Complete the sandbox payment

You can complete this purchase safely because Stripe sandbox test values do not move funds. The hosted form still follows the customer-facing checkout flow.

  • Enter 4242 4242 4242 4242 as the card number.
  • Enter any future expiry date.

The form now shows a completed card number plus a future expiry date. Those visible values confirm the test card details are being accepted.

  • Enter any three-digit CVC.
  • Select the payment button at the bottom of the hosted page.

The browser leaves Stripe Checkout for https://example.com/success. That redirect is the customer-facing success signal.

Check the durable order

DynamoDB preserves the backend order independently of the browser. Checking that record tests whether payment completion reached fulfillment.

  • Return to the PowerShell window from earlier.

Before you run the lookup, do you expect the durable order to match the successful payment?

  • Query the order created for this Checkout Session by running:
Invoke-RestMethod "$env:API_URL/orders/$($checkout.orderId)"

What does this result prove?

You should see status equal to PENDING. You should also see fulfillmentCount equal to 0.

This is the intended shortfall. The hosted redirect completed without sending a trusted payment event to fulfillment.

No Stripe webhook endpoint is registered. The deployed stack still uses whsec_not_configured as its webhook secret.

You found the exact reliability gap. A customer can finish checkout while the durable order stays untouched.

Cannot retrieve the order?

  • Confirm that API_URL is still set in the current PowerShell window.
  • Return to the PowerShell window where you created the Checkout Session if $checkout is undefined.
  • Create a fresh session by rerunning the checkout command above if the order lookup reports that the order was not found.

Still stuck? Help me troubleshoot the checkout order lookup.

The payment flow works while fulfillment still lacks a trusted signal. Next, you'll connect that missing signal to the backend.

Queue Verified Payment Events

The payment from the previous step completed in Stripe Checkout. Its matching Amazon DynamoDB order stayed PENDING with fulfillmentCount set to 0.

A verified webhook gives your backend a trusted payment signal. Amazon SQS holds each accepted event until the fulfillment Lambda updates the order.

In this step, get ready to:
  • Register a Stripe sandbox webhook for completed Checkout Sessions.
  • Redeploy the secure-checkout stack with the returned signing secret.
  • Verify a new payment reaches a PAID order through Amazon SQS.
Register the webhook endpoint

Stripe needs a destination for completed payment events. The existing scripts/register-webhook.mjs script registers your deployed /webhook route for that purpose.

The script returns a live signing secret once. Keep the PowerShell output private because the secret lets your backend verify webhook requests.

  • Return to the PowerShell window from the previous step.
  • Use the existing registration script against your deployed API by running:
$webhook = node scripts/register-webhook.mjs $env:API_URL | ConvertFrom-Json

What does the registration script do?

  • The command passes $env:API_URL to the registration script.
  • The script appends /webhook to the API URL.
  • The endpoint subscribes to checkout.session.completed events.
  • The endpoint uses the description Secure Checkout sandbox webhook.
  • ConvertFrom-Json stores the returned endpoint details in $webhook.

You'll see one JSON object from Stripe. It contains an endpoint id plus a signing secret.

  • Store the returned signing secret in the current PowerShell session by running:
$env:STRIPE_WEBHOOK_SECRET = $webhook.secret

Where does the secret go?

  • $webhook.secret contains the signing secret returned by Stripe.
  • The assignment stores the secret in STRIPE_WEBHOOK_SECRET for the current PowerShell process.
  • src/webhook.mjs reads this value after the stack is redeployed.

That closes the missing destination gap. The endpoint now exists with its signing secret held only in your current PowerShell session.

Webhook registration did not return details?

  • Confirm that you returned to the PowerShell session containing API_URL.
  • Confirm that the same session still contains STRIPE_SECRET_KEY.
  • Confirm that your Stripe account is using its sandbox.

Still stuck? Help me troubleshoot the Stripe webhook registration script.

Redeploy with the signing secret

Your local webhook handler already contains signature verification. Your local fulfillment handler already contains the queue consumer.

AWS SAM packages the existing project for deployment. Redeploying replaces whsec_not_configured with the real signing secret.

  • Rebuild the existing serverless application by running:
sam build

What does the build prepare?

The build prepares the application for deployment from the existing template.yaml. It packages the four Lambda handlers with their dependencies.

You should see AWS SAM report a successful build before PowerShell returns to the prompt.

The stack update can take a few minutes while CloudFormation refreshes the Lambda configuration. A short wait here is expected.

  • Redeploy the secure-checkout stack with both Stripe values by running:
sam deploy --stack-name secure-checkout --resolve-s3 --capabilities CAPABILITY_IAM --region us-east-1 --parameter-overrides "StripeSecretKey=$env:STRIPE_SECRET_KEY" "StripeWebhookSecret=$env:STRIPE_WEBHOOK_SECRET" --no-confirm-changeset --no-fail-on-empty-changeset

What changes in this deployment?

  • --stack-name secure-checkout updates the stack that already hosts your checkout API.
  • StripeSecretKey=$env:STRIPE_SECRET_KEY keeps Checkout Session creation connected to the Stripe sandbox.
  • StripeWebhookSecret=$env:STRIPE_WEBHOOK_SECRET supplies the secret used for signature verification.
  • --region us-east-1 targets the region containing your deployed resources.

You should see the stack update finish without a failed resource. The API URL stays the same because this deployment updates the existing stack.

Deployment did not finish?

  • Confirm that sam build completed before the deployment started.
  • Confirm that STRIPE_WEBHOOK_SECRET was assigned in this PowerShell window.
  • Confirm that your temporary AWS session is still active.

Need another pair of eyes? Help me diagnose my secure-checkout stack update.

Complete a queued payment

The webhook can now accept a trusted payment event. That event crosses the queue boundary before fulfillment changes the order.

How does the payment event move?

  • webhook.handler receives the raw request body plus the stripe-signature header.
  • stripe.webhooks.constructEvent verifies the request before the event is accepted.
  • A valid checkout.session.completed event writes a message to PaymentQueue.
  • The message carries eventId, orderId, and sessionId.
  • fulfillment.handler processes one SQS record at a time.
  • The queue uses maxReceiveCount: 3 before dead-letter handling through PaymentDeadLetterQueue.

This payment stays inside the Stripe sandbox. No real funds move when you use the test card details.

  • Create a second order through the checkout endpoint by running:
$checkout = Invoke-RestMethod -Method Post -Uri "$env:API_URL/checkout" -ContentType "application/json" -Body '{"quantity":1}'

What does this request create?

The request creates a new DynamoDB order with PENDING status. It stores the response in $checkout for the remaining checks.

  • Inspect the new checkout response by running:
$checkout

What should the response contain?

  • orderId identifies the new durable order.
  • status starts as PENDING.
  • checkoutUrl points to the hosted Stripe payment page.
  • Open the new hosted payment page by running:
Start-Process $checkout.checkoutUrl

What does this command open?

The command opens the hosted Checkout page for this new order. The page uses the fixed Cloud Mug price supplied by your backend.

  • Enter 4242 4242 4242 4242 in the card number field.
  • Enter any future expiry date.
  • Enter any three-digit CVC.
  • Submit the sandbox payment.
  • Return to the PowerShell window from earlier.
  • Give the queued event five seconds to reach the worker by running:
Start-Sleep -Seconds 5

Why wait five seconds?

The pause gives Stripe time to deliver the webhook. It also gives Amazon SQS time to invoke the fulfillment worker.

Before you query the order, do you expect its fulfillment state to match the earlier payment?

  • Query the new order by running:
Invoke-RestMethod "$env:API_URL/orders/$($checkout.orderId)"

What proves fulfillment worked?

  • status should equal PAID.
  • fulfillmentCount should equal 1.
  • paymentEventId should identify the accepted Stripe event.
  • paidAt should record when the worker processed payment.
  • The earlier demonstration order should remain PENDING.

You have closed the missing payment signal gap. A verified Stripe event now reaches durable fulfillment through Amazon SQS.

Order still shows PENDING?

  • Confirm that you queried the new value in $checkout.orderId.
  • Confirm that the webhook endpoint targets your current API_URL.
  • Confirm that the redeployment used the current STRIPE_WEBHOOK_SECRET value.
  • Wait another five seconds before querying the same new order again.

Still seeing the old state? Help me trace the event from Stripe through SQS to DynamoDB.

Your checkout now turns a verified payment into queued fulfillment. Next, you'll replace manual deployments with a keyless GitHub Actions pipeline.

Automate Deployment with OIDC

Your checkout now turns verified payment events into paid orders through a queue. The next goal is to make every reviewed push deploy that working system consistently.

Manual deployments are slow and prone to missed settings. A GitHub Actions CI/CD pipeline can test each push before deploying it to AWS. OpenID Connect (OIDC) gives the workflow short-lived credentials without storing AWS access keys.

In this step, get ready to:
  • Deploy the GitHub OIDC provider and its AWS deployment role.
  • Configure the repository secrets and deployment workflow.
  • Push a product change through the pipeline and verify the deployed response.
Deploy the OIDC trust stack

A separate CloudFormation template creates the identity bridge between GitHub and AWS. Its trust policy limits access to your repository's immutable owner ID and repository ID on the main branch.

Why OIDC for deployment?

OIDC lets each workflow run request temporary credentials from AWS. The deployment role accepts that request only when the token matches your repository and branch.

Long-lived AWS access keys never enter the repository. A successful trust check becomes the gate to deployment.

  • Press the Windows key to open Windows search.
  • Type Visual Studio Code into the search bar.
  • Press Enter to start Visual Studio Code.
  • Select File from the top menu.
  • Select Open Folder.
  • Choose your existing secure-checkout folder.
  • Create oidc-bootstrap.yaml inside the secure-checkout folder from the Explorer sidebar.
  • Add the template metadata and repository parameters to oidc-bootstrap.yaml by copying this first section:
AWSTemplateFormatVersion: "2010-09-09"
Description: GitHub OIDC provider and deployment role for Secure Checkout

Parameters:
  GitHubOwner:
    Type: String
  GitHubOwnerId:
    Type: String
  GitHubRepository:
    Type: String
  GitHubRepositoryId:
    Type: String

What does this section define?

  • The template description identifies the infrastructure this stack owns.
  • The four parameters keep the trust policy tied to the GitHub account and repository supplied during deployment.
  • The immutable IDs protect the trust relationship if a GitHub username or repository name is later reused.
  • Save oidc-bootstrap.yaml.
  • Confirm the editor shows the four parameter names under Parameters.
  • Add the OIDC provider and deployment role below the parameter section:

Resources:
  GitHubOidcProvider:
    Type: AWS::IAM::OIDCProvider
    Properties:
      Url: https://token.actions.githubusercontent.com
      ClientIdList:
        - sts.amazonaws.com

  GitHubDeployRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: SecureCheckoutGitHubDeployRole
      MaxSessionDuration: 3600
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Federated: !Ref GitHubOidcProvider
            Action: sts:AssumeRoleWithWebIdentity
            Condition:
              StringEquals:
                token.actions.githubusercontent.com:aud: sts.amazonaws.com
                token.actions.githubusercontent.com:sub: !Sub "repo:${GitHubOwner}@${GitHubOwnerId}/${GitHubRepository}@${GitHubRepositoryId}:ref:refs/heads/main"

How does the trust policy work?

  • The OIDC provider recognizes tokens issued by GitHub for the AWS security token service.
  • The IAM role allows web identity assumption through that provider.
  • The subject condition restricts the role to your immutable owner ID and repository ID on refs/heads/main.
  • Save oidc-bootstrap.yaml.
  • Switch back to the PowerShell window from earlier.
  • Validate the first complete version of the template by running:
sam validate --template-file oidc-bootstrap.yaml

What does this validation prove?

The command checks whether AWS SAM can parse oidc-bootstrap.yaml as a template. A successful result confirms the parameters and role trust policy form valid YAML.

You should see confirmation that the template is valid.

  • Add the deployment policy under the role's existing Properties section:
      Policies:
        - PolicyName: SecureCheckoutDeployment
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - cloudformation:*
                  - s3:*
                  - lambda:*
                  - apigateway:*
                  - dynamodb:*
                  - sqs:*
                  - logs:*
                  - iam:CreateRole
                  - iam:DeleteRole
                  - iam:GetRole
                  - iam:PassRole
                  - iam:PutRolePolicy
                  - iam:DeleteRolePolicy
                  - iam:AttachRolePolicy
                  - iam:DetachRolePolicy
                  - iam:TagRole
                  - iam:UntagRole
                Resource: "*"

What can the deployment role change?

The inline policy grants the services required by the existing AWS SAM template. It also grants the IAM operations needed to maintain the Lambda execution roles during deployment.

The workflow receives these permissions only after its OIDC token passes the trust policy.

  • Save oidc-bootstrap.yaml.
  • Check the expanded role definition by running:
sam validate --template-file oidc-bootstrap.yaml

What changed in this check?

This validation includes the new inline policy. A valid result confirms its indentation keeps Policies inside GitHubDeployRole.

  • Add the role output at the bottom of oidc-bootstrap.yaml by copying this final section:

Outputs:
  RoleArn:
    Description: Add this value to the AWS_ROLE_ARN GitHub repository secret
    Value: !GetAtt GitHubDeployRole.Arn

Why output the role ARN?

The RoleArn output gives GitHub Actions the exact AWS role to request. You will store this non-secret identifier in a repository secret so the workflow can reference it.

  • Save oidc-bootstrap.yaml.
  • Validate the finished template by running:
sam validate --template-file oidc-bootstrap.yaml

What should the final check show?

You should see confirmation that oidc-bootstrap.yaml is a valid template. That result proves the role output and every earlier section parse together.

Template validation failing?

  • Check that Resources and Outputs begin at the left edge.
  • Check that Policies aligns with the other properties under GitHubDeployRole.
  • Confirm every indentation level uses spaces consistently.

Need another pair of eyes? Help me find the YAML structure problem in my OIDC bootstrap template.

✔️ Awesome, I've got everything!

Your saved oidc-bootstrap.yaml now defines the GitHub identity provider and its deployment role.

ⓧ I'd like to double check the full code

Compare your file with this complete version. Keep every indentation level and identifier identical.

AWSTemplateFormatVersion: "2010-09-09"
Description: GitHub OIDC provider and deployment role for Secure Checkout

Parameters:
  GitHubOwner:
    Type: String
  GitHubOwnerId:
    Type: String
  GitHubRepository:
    Type: String
  GitHubRepositoryId:
    Type: String

Resources:
  GitHubOidcProvider:
    Type: AWS::IAM::OIDCProvider
    Properties:
      Url: https://token.actions.githubusercontent.com
      ClientIdList:
        - sts.amazonaws.com

  GitHubDeployRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: SecureCheckoutGitHubDeployRole
      MaxSessionDuration: 3600
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Federated: !Ref GitHubOidcProvider
            Action: sts:AssumeRoleWithWebIdentity
            Condition:
              StringEquals:
                token.actions.githubusercontent.com:aud: sts.amazonaws.com
                token.actions.githubusercontent.com:sub: !Sub "repo:${GitHubOwner}@${GitHubOwnerId}/${GitHubRepository}@${GitHubRepositoryId}:ref:refs/heads/main"
      Policies:
        - PolicyName: SecureCheckoutDeployment
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - cloudformation:*
                  - s3:*
                  - lambda:*
                  - apigateway:*
                  - dynamodb:*
                  - sqs:*
                  - logs:*
                  - iam:CreateRole
                  - iam:DeleteRole
                  - iam:GetRole
                  - iam:PassRole
                  - iam:PutRolePolicy
                  - iam:DeleteRolePolicy
                  - iam:AttachRolePolicy
                  - iam:DetachRolePolicy
                  - iam:TagRole
                  - iam:UntagRole
                Resource: "*"

Outputs:
  RoleArn:
    Description: Add this value to the AWS_ROLE_ARN GitHub repository secret
    Value: !GetAtt GitHubDeployRole.Arn

How to use this reference

Check the parameters first. Then compare the provider, role, policy, and output in order.

  • Record your GitHub username here: your GitHub username.
  • Replace YOUR-GITHUB-USERNAME in the command below with your GitHub username.
  • Retrieve the repository record by running:
$repo = Invoke-RestMethod "https://api.github.com/repos/YOUR-GITHUB-USERNAME/secure-checkout"

What does this request return?

GitHub returns the repository record and stores it in $repo. The record includes the immutable owner ID and repository ID required by the trust policy.

  • Record $repo.owner.id here: your GitHub owner ID.
  • Record $repo.id here: your GitHub repository ID.
  • Replace YOUR-GITHUB-USERNAME in the deployment command with your GitHub username.
  • Replace YOUR-OWNER-ID with your GitHub owner ID.
  • Replace YOUR-REPOSITORY-ID with your GitHub repository ID.
  • Deploy the OIDC bootstrap stack by running the updated command:
sam deploy --template-file oidc-bootstrap.yaml --stack-name secure-checkout-oidc --capabilities CAPABILITY_NAMED_IAM --region us-east-1 --parameter-overrides GitHubOwner=YOUR-GITHUB-USERNAME GitHubOwnerId=YOUR-OWNER-ID GitHubRepository=secure-checkout GitHubRepositoryId=YOUR-REPOSITORY-ID --no-confirm-changeset --no-fail-on-empty-changeset

What does this deployment create?

  • The secure-checkout-oidc stack creates the GitHub OIDC provider.
  • The stack creates SecureCheckoutGitHubDeployRole with the repository-specific trust policy.
  • The CAPABILITY_NAMED_IAM acknowledgement permits CloudFormation to create the named role.

The deployment can take a few minutes while CloudFormation creates the identity resources. The final stack outputs include RoleArn when the deployment succeeds.

  • Record the RoleArn stack output here: your deployment role ARN.

OIDC stack deployment failing?

  • Confirm the deployment command uses the numeric owner ID from $repo.owner.id.
  • Confirm the command uses the numeric repository ID from $repo.id.
  • Check that your AWS session is still active in the same PowerShell window.

Still blocked? Help me troubleshoot my secure-checkout-oidc stack deployment.

Add repository secrets and the workflow

The workflow needs the deployment role ARN and the two existing Stripe sandbox values. GitHub repository secrets keep those values outside .github/workflows/deploy.yml.

Credential setup can feel risky. You will copy each Stripe value directly from the current PowerShell session without printing it in the terminal.

  • Return to the existing secure-checkout repository on GitHub.
  • Select Settings.
  • Select Secrets and variables.
  • Select Actions.
  • Select the Secrets tab.
  • Select New repository secret.
  • Enter AWS_ROLE_ARN in the Name field.
  • Paste your deployment role ARN into the Secret field.
  • Select Add secret.

What does this secret identify?

The AWS_ROLE_ARN repository secret tells the workflow which role to assume. The OIDC trust policy still decides whether the request is accepted.

  • Switch back to the current PowerShell session.
  • Copy the Stripe sandbox secret key to the Windows clipboard by running:
$Env:STRIPE_SECRET_KEY | Set-Clipboard

Why use the clipboard command?

The command copies the value stored in STRIPE_SECRET_KEY without printing it. Your live sandbox credential stays out of the visible terminal output.

  • Return to the repository secrets page.
  • Select New repository secret.
  • Enter STRIPE_SECRET_KEY in the Name field.
  • Paste the clipboard value into the Secret field.
  • Select Add secret.
  • Switch back to the current PowerShell session.
  • Copy the Stripe webhook signing secret to the Windows clipboard by running:
$Env:STRIPE_WEBHOOK_SECRET | Set-Clipboard

What value is being copied?

This copies the signing secret returned when you registered the existing sandbox webhook endpoint. The workflow passes it back to the deployed stack during each update.

  • Return to the repository secrets page.
  • Select New repository secret.
  • Enter STRIPE_WEBHOOK_SECRET in the Name field.
  • Paste the clipboard value into the Secret field.
  • Select Add secret.

You should now see three masked repository secret entries. Their values remain hidden on this page.

The deployment workflow is a YAML file inside .github/workflows. GitHub reads that path automatically when commits reach the repository.

  • Switch back to Visual Studio Code.
  • Initialize the existing secure-checkout folder as a Git repository by running this command in PowerShell:
git init -b main

What does this command do?

The command starts local version tracking in secure-checkout. It creates the branch named main so the local branch matches the workflow trigger and OIDC trust policy.

  • Create .github/workflows/deploy.yml inside the existing secure-checkout folder from the Explorer sidebar.
  • Add the workflow triggers and Node.js setup by copying this first section:
name: Test and deploy

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

concurrency:
  group: secure-checkout-deploy
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7

      - name: Set up Node.js
        uses: actions/setup-node@v7
        with:
          node-version: 24
          package-manager-cache: false

What does the workflow foundation do?

  • The workflow starts on pushes to main or a manual dispatch.
  • The permissions allow OIDC token requests and repository checkout.
  • The concurrency group keeps deployments for this project from overlapping.
  • The first steps check out the source and select Node.js 24.
  • Save .github/workflows/deploy.yml.
  • Add dependency installation, tests, AWS SAM CLI setup, and OIDC authentication below the existing steps:

      - name: Install dependencies
        run: npm install --ignore-scripts --package-lock=false

      - name: Run tests
        run: npm test

      - name: Set up AWS SAM CLI
        uses: aws-actions/setup-sam@v3
        with:
          version: 1.166.2
          use-installer: true
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Configure short-lived AWS credentials
        uses: aws-actions/configure-aws-credentials@v6.3.0
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          role-session-name: secure-checkout-${{ github.run_id }}
          aws-region: us-east-1

How does the workflow reach AWS?

  • The install step recreates the Node.js dependencies on the hosted runner.
  • The test step stops the workflow if the product tests fail.
  • The setup action installs AWS SAM CLI 1.166.2.
  • The credential action requests the role named by AWS_ROLE_ARN through OIDC.
  • Save .github/workflows/deploy.yml.
  • Confirm Git can see the new workflow path by running:
git status --short

What should Git report?

The short status output should show .github as untracked content. That confirms the workflow file sits inside the repository Git is tracking.

  • Add the build and deployment steps at the bottom of .github/workflows/deploy.yml by copying this final section:

      - name: Build
        run: sam build

      - name: Deploy
        env:
          STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
          STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
        run: >
          sam deploy
          --stack-name secure-checkout
          --resolve-s3
          --capabilities CAPABILITY_IAM
          --region us-east-1
          --parameter-overrides
          "StripeSecretKey=$STRIPE_SECRET_KEY"
          "StripeWebhookSecret=$STRIPE_WEBHOOK_SECRET"
          --no-confirm-changeset
          --no-fail-on-empty-changeset

How does the deployment finish?

The build step prepares the AWS SAM application. The deployment step updates secure-checkout in us-east-1 with the two encrypted Stripe values.

The same template keeps API Gateway, Lambda, DynamoDB, and SQS aligned with the repository.

  • Save .github/workflows/deploy.yml.

✔️ Awesome, I've got everything!

Your saved workflow now tests, authenticates, builds, and deploys every push to main.

ⓧ I'd like to double check the full code

Compare your workflow with this complete version. Pay close attention to indentation under steps and run.

name: Test and deploy

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

concurrency:
  group: secure-checkout-deploy
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7

      - name: Set up Node.js
        uses: actions/setup-node@v7
        with:
          node-version: 24
          package-manager-cache: false

      - name: Install dependencies
        run: npm install --ignore-scripts --package-lock=false

      - name: Run tests
        run: npm test

      - name: Set up AWS SAM CLI
        uses: aws-actions/setup-sam@v3
        with:
          version: 1.166.2
          use-installer: true
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Configure short-lived AWS credentials
        uses: aws-actions/configure-aws-credentials@v6.3.0
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          role-session-name: secure-checkout-${{ github.run_id }}
          aws-region: us-east-1

      - name: Build
        run: sam build

      - name: Deploy
        env:
          STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
          STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
        run: >
          sam deploy
          --stack-name secure-checkout
          --resolve-s3
          --capabilities CAPABILITY_IAM
          --region us-east-1
          --parameter-overrides
          "StripeSecretKey=$STRIPE_SECRET_KEY"
          "StripeWebhookSecret=$STRIPE_WEBHOOK_SECRET"
          --no-confirm-changeset
          --no-fail-on-empty-changeset

How to use this reference

Compare the triggers and permissions first. Then check every step from repository checkout through deployment.

Push and verify the pipeline

The local repository now contains the application, OIDC template, and workflow. Publishing that exact state triggers the first automated deployment.

Git may ask you to authenticate to GitHub during the first push. Complete the browser sign-in if that prompt appears.

  • Replace YOUR-GITHUB-USERNAME in the remote URL below with your GitHub username.
  • Commit the project and push it to the existing GitHub repository by running the updated commands:
git add .
git commit -m "Build secure serverless checkout"
git remote add origin https://github.com/YOUR-GITHUB-USERNAME/secure-checkout.git
git push -u origin main

What does this publish?

  • The first command stages every project file allowed by .gitignore.
  • The commit records the complete checkout application and deployment configuration.
  • The remote command connects the local repository to GitHub.
  • The push publishes main and triggers Test and deploy.

The first workflow run can take several minutes while the hosted runner installs dependencies and AWS SAM CLI. A quiet step during installation is expected.

  • Return to the secure-checkout repository on GitHub.
  • Select Actions.
  • Select the latest Test and deploy run.
  • Confirm every job finishes successfully before continuing.

The first green run proves the repository can test the code and assume the AWS role. A source change now tests whether that path updates the live API.

  • Switch back to Visual Studio Code.
  • Choose a short new product description here: your new product description.
  • In src/product.mjs, replace only the current text assigned to description with your new product description.
  • Save src/product.mjs.
  • Commit the product update and push it by running:
git add .
git commit -m "Build secure serverless checkout"
git push -u origin main

What happens after this push?

The push starts a second workflow run. That run executes the product tests before OIDC grants temporary AWS credentials.

A successful deployment replaces the catalog function code while preserving the queue, webhook, and order data.

  • Return to the repository's Actions page.
  • Select the newest Test and deploy run.
  • Wait for every step to finish successfully.

Workflow run failing?

  • Confirm the repository contains .github/workflows/deploy.yml on the main branch.
  • Confirm the repository secrets are named AWS_ROLE_ARN, STRIPE_SECRET_KEY, and STRIPE_WEBHOOK_SECRET.
  • Confirm the owner ID and repository ID in secure-checkout-oidc belong to this repository.

Need help with the failed step? Help me diagnose my GitHub Actions OIDC deployment failure.

Before you call the catalog endpoint, what product description do you expect the deployed function to return?

  • Query the deployed catalog endpoint from the current PowerShell session by running:
Invoke-RestMethod "$env:API_URL/products/demo"

What should the endpoint return?

The response should still identify the Cloud Mug with unitAmount equal to 1500 and currency equal to usd.

Its description should match your new product description.

That is the full delivery loop working. Your source change passed its tests and reached the live AWS endpoint through short-lived OIDC credentials.

Still seeing the previous description?

  • Confirm the newest workflow run finished its Deploy step successfully.
  • Confirm API_URL still points to the secure-checkout stack in us-east-1.
  • Confirm the latest commit on main contains your saved src/product.mjs change.

Still getting the old value? Help me trace why my deployed catalog endpoint has not updated.

Secret mission

Make Fulfillment Idempotent

Stripe or Amazon SQS can deliver the same payment event more than once. Add an atomic DynamoDB condition that lets the first delivery fulfill the order while every duplicate leaves fulfillmentCount at 1.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep your checkout resources running, pause activity, or delete everything. The demo is expected to cost $0 at this scale on the AWS Free Plan, though persistent cloud resources still consume allowances or promotional credits.

Cost warning

Your AWS Free Plan ends after six months or when its credits are exhausted. An account that creates or joins AWS Organizations automatically upgrades to a paid plan.

The upgrade also expires any remaining Free Tier credits.

  • Keep this account outside AWS Organizations unless you intend to upgrade it.

Resources you used:

  • The secure-checkout AWS CloudFormation stack. It contains an Amazon API Gateway HTTP API, four AWS Lambda functions, an Amazon DynamoDB table, an Amazon SQS queue, and a dead-letter queue.
  • The secure-checkout-oidc AWS CloudFormation stack. It contains the GitHub OIDC provider and deployment role.
  • The Stripe sandbox webhook endpoint with the description Secure Checkout sandbox webhook.
  • The secure-checkout repository on GitHub. It contains the deployment workflow and repository secrets.
  • The local secure-checkout folder containing your project code.

Keep everything running

No action needed. Choose this if you still want to demo the checkout or continue using the deployment pipeline.

  • Keep the secure-checkout stack deployed.
  • Keep the secure-checkout-oidc stack deployed.
  • Leave the Stripe sandbox webhook endpoint enabled.
  • Keep the GitHub repository and its repository secrets.
  • Keep the local secure-checkout folder.
  • Monitor the AWS Cost and Usage widget for changes to your remaining allowances or credits.

Pause - I'll come back to this later

Pause external payment events while keeping the deployed infrastructure and project files ready for later.

  • Return to the Stripe sandbox webhook endpoint with the description Secure Checkout sandbox webhook.
  • Use the endpoint's disable control.
  • Confirm the endpoint shows a disabled state.
  • Stop pushing commits to main while the project is paused.
  • Keep both AWS CloudFormation stacks deployed.
  • Keep the GitHub repository and local secure-checkout folder.

The checkout API remains deployed during the pause. Its AWS resources continue consuming Free Plan allowances or promotional credits.

Delete - I don't want to use this again

Deleting these resources is final. Your order records disappear when the application stack is removed.

CloudFormation deletion can take a while. PowerShell may stay busy while AWS removes dependent resources.

  • Expect AWS SAM to ask for deletion confirmation.
  • Approve each deletion prompt.
  • Delete both AWS CloudFormation stacks by running these commands in the signed-in PowerShell window:
sam delete --stack-name secure-checkout --region us-east-1
sam delete --stack-name secure-checkout-oidc --region us-east-1

What Do These Commands Remove?

  • The first command removes the application stack and its AWS SAM deployment artifacts.
  • The second command removes the GitHub OIDC provider and deployment role.
  • Wait for both commands to finish.
  • Confirm PowerShell reports no deletion failure for either stack.

Stack Deletion Failed?

  • Repeat the AWS browser sign-in from Step 1 if your temporary session expired.
  • Retry the failed stack command after sign-in completes.

Help me diagnose the stack deletion.

  • Return to the Stripe sandbox webhook endpoint with the description Secure Checkout sandbox webhook.
  • Use the endpoint's deletion control.
  • Confirm the endpoint no longer appears in the sandbox webhook list.
  • Return to the secure-checkout repository on GitHub.
  • Open Settings.
  • Use the repository deletion control to remove secure-checkout.
  • Confirm GitHub no longer shows the repository under your account.
  • Use File Explorer to delete the local secure-checkout folder.
  • Confirm the folder no longer appears in File Explorer.

Nice Work!

Nice Work!

You made it! You built a secure serverless checkout that processes payments in the Stripe sandbox through AWS Lambda. Your GitHub Actions pipeline now deploys the system with temporary AWS credentials through OIDC.

You've learned how to:

  • Built four focused AWS Lambda services behind an Amazon API Gateway HTTP API. Stored durable order state in Amazon DynamoDB.
  • Created Stripe-hosted Checkout Sessions with a fixed server-side price. Verified webhook signatures before sending payment events through Amazon SQS to a worker with a dead-letter queue.
  • Added automated tests and deployments for every push to main. Authenticated deployments with a keyless OIDC role that issues temporary AWS credentials.
  • Completed the Secret Mission by adding atomic idempotency to the fulfillment worker. Proved that duplicate signed payment events leave fulfillmentCount at 1.

Ready to quiz yourself?