Stripe checkout and cart
Stripe Checkout + cart
For when the answer to “It takes payments” is yes.
Single-merchant checkout where the SERVER decides the price, a signature-verified webhook, and a cart drawer.
Built by hand again in 10 of 20 audited projects before this existed. Each rebuild was another chance to make the mistake below.
Skip the rebuild
You do not write this one. It arrives in your project as ordinary source you can read, change and keep, with its audit record and its caveats beside it.
- Download the Playground. It is free and runs on your own machine.
- Start a new app and tick “It takes payments”.
- This block is written in, along with anything else you ticked.
The mistake it removes
High-value and easy to get wrong: a checkout that trusts a client-supplied price is a free-money bug. Rebuilt in ~10/20 projects.
What is already handled
Each of these was checked by running the code, not by reading it.
- unit_amount is only ever p.price_cents from the server catalog — mechanically checked, not eyeballed
- no client price/amount field is read anywhere in the handler
- unknown item id rejected (including __proto__/constructor, which a plain-object index let through); qty clamped to 1–99; server-side price sanity-checked; at most 20 line items per cart
- ONE clamped quantity feeds both the charge and metadata.cart, so the fulfillment record cannot disagree with what was paid for
- SITE_URL is required and the redirect is never derived from request headers, so a caller cannot aim the page a paying customer lands on
- webhook verifies its Stripe signature against the raw body
What the audit found
Named rather than summarized. The reasoning behind each one ships inside the block, so it travels with the code instead of living on a page you have to trust.
- The previous DD record graded a version of this block that was later rewritten major · Provenance
- Nothing in this repository executes a line of the checkout major · Verification integrity
- No live test-mode purchase has ever been run major · Correctness
- Order dedupe is a seam, not a finished feature minor · Data durability
- No rate limit and no caller identity on the checkout endpoint minor · API cost & abuse safety
What you still have to do
A copied file cannot own your secrets, your host or your legal obligations. This is the part the block deliberately does not claim.
- Keep STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in your host's secret store, serve the webhook over HTTPS at the endpoint registered in Stripe, and replace the in-process fulfillment log with a unique index on session_id in your own store before you take real money. The price-trust rule and the signature check are shipped; secret custody and durable dedupe are not things a copied file can own.
- Your platform's rate limiting in front of the checkout endpoint — a Vercel firewall rule, Cloudflare, or an API gateway limit — before launch. The handler bounds a cart at 20 line items and caps metadata.cart under Stripe's 500-character limit, but it has no rate limit and no caller identity: anyone who can reach it can loop it and mint Checkout Sessions against your Stripe account.
- A live test-mode purchase has NOT been run here — that needs your Stripe keys. The code is the version DD verified in situ; the end-to-end money path should still be exercised once with a test card before real launch.
- Single merchant only: Connect fields (transfer_data, application_fee_amount) were deliberately removed.
- Order dedupe is a SEAM, not a finished feature. Stripe delivers a webhook at least once, so a retry is a real duplicate with a valid signature; `_fulfillment-log.js` ships an in-process default that stops a duplicate hitting the SAME running instance and nothing more — it does not survive a cold start or a second instance. Replace it with a unique index on session_id in your own store before you take real money (the file shows the four-line version).
What lands in your project
node
- node/create-checkout-session.js
- node/stripe-webhook.js
- node/_checkout-core.js
- node/_fulfillment-log.js
Serverless handlers (Vercel-style). Reads prices from YOUR catalog.json; the browser only sends item ids and quantities. Requires the `stripe` npm package (npm i stripe) — declared in stacks.node.dependencies. The two `_`-prefixed files are helpers, not endpoints: every decision the checkout makes lives in `_checkout-core.js` (no SDK, so it is testable), and `_fulfillment-log.js` is the dedupe seam the webhook claims each session through. Vercel skips `_`-prefixed files under /api rather than publishing them as functions.
vanilla
- vanilla/cart.js
- vanilla/catalog.example.json
Cart drawer + the catalog shape. Rename catalog.example.json to catalog.json and put it where the handler reads it (../assets/catalog.json).
Needs stripe ^17 from npm.
What you supply
- STRIPE_SECRET_KEY — sk_… (test key first)
- STRIPE_WEBHOOK_SECRET — whsec_… from the dashboard endpoint
- SITE_URL — REQUIRED. Your own origin, e.g. https://your-domain.com — where a paying customer is sent back to. There is no fallback on purpose: deriving it from request headers let an attacker choose that page (ph105tsk09), and checkout fails closed with a message if it is unset.
- catalog.json — your products: id, name, price_cents (integer, >= 50), kind
Licensed MIT. It is a starting point, not a finished product.
Get it
Download the Playground See the other blocks
Nothing here is locked. The files are yours, in your folder, under a permissive license.
Questions
- Is Stripe checkout and cart audited?
- It was audited on 2026-08-28, and the verdict was not a clean pass.
- What do I still have to do myself?
- Keep STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET in your host's secret store, serve the webhook over HTTPS at the endpoint registered in Stripe, and replace the in-process fulfillment log with a unique index on session_id in your own store before you take real money. The price-trust rule and the signature check are shipped; secret custody and durable dedupe are not things a copied file can own.
- How do I get this code?
- Download the Playground, start a new app, and tick “It takes payments”. The block is written into your project as ordinary source you can read and edit.
All pre-built code blocks · Learn to build from zero · The coding guide