Storage and Catalog Drivers: SQLite, DynamoDB, S3
Two Data Types, Two Lifecycles
Separating catalog from state is the architectural decision the rest of the module leans on. The catalog changes when you change it, is identical for every shopper, and must be the single authority on prices, so it is modeled as a read-only source with caching, never written by the commerce code. State is the opposite: per-shopper, constantly mutated, and worthless after a short window, a cart matters for the length of a conversation, a session for the length of a payment, an idempotency record for the length of a retry horizon. Modeling state as TTL-expiring rows means the store cleans itself, holds nothing a breach could monetize (no card data, no order archive), and never grows unboundedly. Orders deliberately exit the system through the order webhook instead of accumulating in a table.
The Store Interfaces
Three small contracts cover all mutable state. CartStore and CheckoutSessionStore are get, put with a TTL, and delete. IdempotencyStore is the interesting one: begin(scope, key, bodyHash) reserves a key and reports whether this request is new, a replay (returning the stored response), a conflict (same key, different body), or in flight, with complete storing the final response and abort releasing the reservation after a server error. Those four verbs are exactly the semantics the ACP idempotency rules require, and implementing them once per backend keeps the router logic identical everywhere. Embedders can implement any of the three interfaces against Redis, Postgres, or whatever already runs in their stack, each is a handful of methods.
SQLite: The Single-Box Default
The default stores use better-sqlite3, a synchronous, in-process SQLite binding that is more than fast enough for commerce traffic and removes an entire network dependency from the deployment. With no configuration, all three stores run in memory, perfect for tests and demos, gone on restart. Pointing the CART_DB, SESSION_DB, and IDEMPOTENCY_DB environment variables at file paths makes them durable, and file-backed databases switch on write-ahead logging for sane concurrent behavior. Expiry is lazy plus sweepable: expired rows are deleted when read, and the cart store exposes a sweep() method a cron can call to clear the long tail, with row counts so you can log what was removed.
SQLite is the right answer for the Docker container, the EC2 box, and the VPS, one process, local disk, no other moving parts. Its limit is multi-instance deployments: two servers cannot share a SQLite file safely across hosts, so the moment you scale horizontally or go serverless, you switch backends, which is one config line.
DynamoDB: The Serverless Pair
Selecting "storage": { "type": "dynamo" } swaps all three stores to DynamoDB tables, named by a configurable prefix (default mcp_commerce_): carts and sessions keyed by a string id, idempotency keyed by a composite string pk, all with TTL enabled on an expires_at epoch-seconds attribute. Dynamo's native TTL does the housekeeping SQLite does lazily, with the same read-time expiry check layered on top because Dynamo deletes expired items eventually rather than instantly. The idempotency store uses conditional writes so the first concurrent request wins the reservation atomically, the property that makes ACP retries safe even across a fleet of Lambda containers.
The pairing details, table creation, permissions, capacity mode, live in the Lambda deployment guide. One packaging note applies beyond Lambda: the AWS SDK packages are optional peer dependencies, installed only when you use them, and the SQLite modules are lazy-loaded only when selected, so a Dynamo deployment never loads the native SQLite binary (good for cold starts and cross-architecture builds), and a SQLite deployment never loads the AWS SDK.
Catalog Sources: Bundled, Remote URL, S3
The catalog side has one interface, load(), and three shipped sources. The bundled source reads a local JSON file, resolved relative to your config file, re-reading every time by default so development edits appear instantly, with an optional cache TTL for production. The remote URL source fetches over HTTPS from a location you control, the fit when a CMS, admin tool, or build pipeline publishes the catalog, cached five minutes by default. The S3 source reads a bucket object, the natural companion to Lambda, also cached five minutes. All three validate the catalog identically on every load, schema, price integrity, and the global id uniqueness rule from the products file reference, and a failed validation rejects the whole load rather than serving a partial store.
Cache TTL is the one knob worth thinking about: it bounds how stale a price can be. Within the window, a cart read can price against the cached catalog; after it, the next read fetches fresh. Five minutes is a sensible default for stores whose prices change rarely, shorten it if you run frequent promotions, lengthen it if your catalog host is slow or rate-limited. Because every cart and session read re-resolves against whatever the source returns, a catalog update propagates to live carts automatically, the anti-staleness behavior described in the security model.
Choosing per Deployment
The combinations cluster into three sane stacks. Laptop and CI: bundled catalog, in-memory SQLite, nothing to clean up. Single server (Docker, EC2, VPS): bundled or remote URL catalog, file-backed SQLite, durable across restarts with zero external services. Serverless or multi-instance: S3 catalog, DynamoDB stores, infinitely horizontal. Mixed positions are fine too, a single server with an S3 catalog so marketing can publish products without a deploy is a popular middle ground. The decision is reversible by design: every stack runs the same core, the same tools, and the same endpoints, so promoting from laptop to Lambda is configuration plus packaging, never a rewrite. Teams sizing infrastructure for a new store will find the broader hosting trade-offs in our website hosting guide.
Writing a Custom Driver
Teams with an existing operational database often prefer one less technology, and the interfaces were sized for that. A Redis cart store is the canonical example: get is a key read with JSON parsing, put is a SET with an expiry matching the TTL argument, delete is a DEL, perhaps thirty lines with error handling. A Postgres idempotency store maps begin to an INSERT with an ON CONFLICT clause that inspects the stored body hash and status, which is the same first-writer-wins shape the DynamoDB driver gets from conditional writes. Two rules keep custom drivers correct. Honor the TTL argument rather than inventing your own expiry policy, the checkout service reasons about session lifetimes through it. And keep begin atomic, the idempotency guarantees are only as strong as the reservation's race behavior under concurrent identical requests. The shipped SQLite drivers are the cleanest reference implementations to copy from, each is one small, dependency-light file in the repository.
