Home » Free ACP Payments Module » Deploy on AWS Lambda

How to Deploy on AWS Lambda

The ACP Payment Module ships a first-class AWS Lambda entry point: front an API Gateway HTTP API (payload format v2) at the exported handler, switch storage to DynamoDB, read the catalog from S3, and the same code that runs the standalone server runs serverless, the MCP endpoint, the ACP checkout API, the Stripe webhook, and the product feed included. Commerce traffic for a software product is naturally spiky and mostly idle, which is exactly the load shape Lambda prices well.

Why Serverless Fits This Workload

A store attached to an MCP tool sees bursts, a launch, a newsletter, an assistant feature rollout, separated by long quiet stretches. A dedicated server idles through the quiet and needs headroom for the bursts; Lambda bills per request and scales horizontally without configuration. The module was shaped for this from the start: requests are stateless, all durable state lives behind store interfaces, dependencies are wired once per warm container and reused, and the SQLite native binary is never even loaded when DynamoDB storage is selected, keeping cold starts lean. The one real constraint is that in-memory or on-disk SQLite cannot survive between invocations, so DynamoDB storage is not optional here, it is the design.

Step 1: Build and package the function.
Compile and bundle:
npm ci
npm run build
Package dist/, your production node_modules (including the AWS SDK packages: @aws-sdk/client-dynamodb, @aws-sdk/lib-dynamodb, and @aws-sdk/client-s3 for the S3 catalog), and your config.json into the function archive. Set the handler to dist/lambda.handler and the runtime to Node.js 20 or newer. Any packaging workflow works, a zip, SAM, CDK, or Serverless Framework, the module imposes nothing beyond the handler path.
Step 2: Create the DynamoDB tables.
Three tables, named with your configured prefix (default mcp_commerce_):
mcp_commerce_carts         partition key: id (String)
mcp_commerce_sessions      partition key: id (String)
mcp_commerce_idempotency   partition key: pk (String)
Enable TTL on the expires_at attribute (epoch seconds) for all three, on-demand capacity mode suits the bursty profile. DynamoDB's TTL deletion is eventual, the stores also check expiry on read, so a not-yet-swept expired cart still behaves as expired. The idempotency table's conditional writes are what serialize concurrent ACP retries, the behavior documented in the endpoints reference.
Step 3: Publish the catalog to S3.
Upload your products.json to a bucket and point the config at it:
"catalog": { "type": "s3", "bucket": "acme-store",
             "key": "products.json", "region": "us-east-1",
             "cacheTtlSeconds": 300 },
"storage": { "type": "dynamo", "region": "us-east-1" }
The source caches per warm container for the TTL, so catalog reads add no per-request latency in steady state, and a catalog update propagates to all containers within the cache window. Updating your store becomes an S3 upload, no redeploy. Validation still applies on every load, a malformed upload is rejected loudly rather than served, see the products file reference.
Step 4: Configure environment and permissions.
Set the same environment variables the server uses, COMMERCE_CONFIG if your config sits anywhere but the default path, plus the secrets: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, ACP_BEARER_TOKEN, and optionally ACP_SIGNING_SECRET and ORDER_WEBHOOK_SECRET, ideally injected from your secrets manager rather than typed into the console. The execution role needs GetItem, PutItem, UpdateItem, and DeleteItem on the three tables and GetObject on the catalog key, nothing wider. The full variable list is in the configuration reference.
Step 5: Front it with an API Gateway HTTP API.
Create an HTTP API (the lighter, cheaper API Gateway flavor) with payload format version 2.0 and a catch-all route, ANY /{proxy+}, integrated with the function. The handler translates the v2 event into the same request and response objects the standalone server routes, headers, raw body, query strings, and base64 bodies all handled, so all four surfaces work immediately: POST /mcp, /checkout_sessions, POST /webhooks/stripe, and GET /feed. Attach your custom domain, then run the same smoke tests as the quickstart: an MCP client against /mcp, a curl session against the ACP API, and a Stripe test payment end to end with the webhook pointed at the new domain.

Lambda-Specific Behavior Worth Knowing

The handler builds its dependency graph, config, catalog source, stores, payment provider, on first invocation and caches the promise for the container's lifetime, so warm requests skip initialization entirely. A failed initialization (a typo'd config, a missing table) is deliberately not cached: the next invocation retries cleanly instead of pinning the container into a permanent 500. Raw bodies survive the API Gateway translation byte-for-byte, including base64-encoded payloads, which matters because both Stripe webhook signatures and ACP request signatures are HMACs over the exact bytes, a re-serializing proxy would break them, this handler does not.

The MCP transport runs in stateless JSON mode, one request, one response, no server-push streaming, which is precisely the shape API Gateway supports and the reason the module's MCP surface needs no special transport configuration on Lambda. Concurrency needs no tuning either: every piece of shared state sits in DynamoDB behind conditional writes, so a hundred parallel containers behave identically to one.

Costs and When a Server Is Better

At typical software-store volumes, the serverless stack is close to free in absolute terms: requests measured in the thousands per month, three small on-demand tables of TTL-expiring rows, and one S3 object fetched on cache misses. The crossover where a dedicated box wins is sustained high request rates, where per-request pricing eventually exceeds a flat instance, and latency-critical paths where cold starts (modest here, but nonzero) are unacceptable. The honest default for most teams reading this page is Lambda for the store even when the main product runs on servers, the store's traffic profile is the serverless poster child. Both deployments are the same codebase and the same config schema, so changing your mind later is a packaging change, not a migration, the comparison details live in storage and catalog drivers.

Webhook reminder: after the domain is live, update the Stripe webhook endpoint URL to the API Gateway domain and re-set STRIPE_WEBHOOK_SECRET to the new endpoint's secret, the webhook guide covers verification and testing against a deployed URL.

Updating a Live Deployment

Day-two operations stay simple because the three concerns update independently. Catalog changes are an S3 upload, live within the cache TTL, no deploy. Configuration changes (TTLs, URLs, feed fields) are a new function version with an updated config.json. Code upgrades are a rebuild and republish, and because all state lives in DynamoDB with stable shapes, old and new function versions can briefly coexist during a rollout without corrupting carts or sessions, an in-flight checkout started on the old version completes fine on the new one. Secrets rotate through your secrets manager with a function restart picking them up; rotate the ACP bearer token in coordination with your platform partner, and the Stripe webhook secret together with the dashboard endpoint, as a mismatch window just delays confirmations rather than losing them. CloudWatch is worth one alarm from day one: the loud error logs, failed order deliveries and unmatched paid webhooks, are the two lines that mean money needs a human.