Choose Arkansas Cart — Cloudflare Worker
This is the server-side half of the shopping cart. The main Jekyll site (served by GitHub Pages) is 100% static and cannot run any server code or hold a secret key — so this small Worker exists purely to:
- Create Stripe Checkout Sessions (
POST /create-checkout-session) — looks up trusted Stripe Price IDs fromsrc/price-map.jsonfor whatever{slug, size, color, qty}the cart sends. It never trusts a price the browser sends. - Handle the Stripe webhook (
POST /stripe-webhook) — verifies the signature, records the order, and is idempotent (a retried Stripe event is never processed twice).
One-time setup
cd cloudflare-worker
npm install
npx wrangler login
Create the KV namespace used for order records + webhook idempotency:
npx wrangler kv namespace create ORDERS_KV
Paste the id it prints into wrangler.toml under kv_namespaces.
Set the two secrets (never put these in wrangler.toml or any committed file):
npx wrangler secret put STRIPE_SECRET_KEY
npx wrangler secret put STRIPE_WEBHOOK_SECRET
Use your Stripe test mode keys first — see Testing below.
For local development, copy .dev.vars.example to .dev.vars (gitignored)
and fill in test values, then:
npm run dev
Deploying
npm run deploy
This prints your Worker’s URL, e.g. https://choose-arkansas-cart.YOUR-SUBDOMAIN.workers.dev.
Copy that URL into the main site’s _config.yml:
cart_worker_url: "https://choose-arkansas-cart.YOUR-SUBDOMAIN.workers.dev"
Commit and push the Jekyll site so the cart JS picks up the new URL. No DNS
changes are required — the Worker runs fine on its *.workers.dev URL. If
you’d rather it live on your own domain (e.g. api.choosearkansas.shop),
that’s a Worker Route, set up separately in the Cloudflare dashboard.
Stripe Price IDs you need to supply
Every variant in _data/products.yml currently has a placeholder Price ID
(anything containing PLACEHOLDER). In Stripe: Products → create a
product per item → add one Price per size (since these are all priced the
same across sizes, you can reuse one Product with multiple Prices, or one
Price per size if you want per-size reporting — either works, since we key
off the Price ID either way).
Steps to wire up real IDs:
- Create the Products/Prices in Stripe (test mode first).
- Paste each real
price_...ID into the matching slot in_data/products.ymlunder that product’sprice_ids:map. - Regenerate the Worker’s trusted copy:
python3 scripts/generate-price-map.py - Redeploy the Worker:
npm run deploy(fromcloudflare-worker/).
The Worker will refuse to check out any variant still flagged as a placeholder, with a clear error message rather than silently charging the wrong amount — so you’ll know immediately if one was missed.
Environment variables / secrets
| Name | Where it’s set | Purpose |
|---|---|---|
STRIPE_SECRET_KEY |
wrangler secret put |
Server-side Stripe API calls. Never exposed to the browser. |
STRIPE_WEBHOOK_SECRET |
wrangler secret put |
Verifies incoming webhook requests are really from Stripe. |
SITE_URL |
wrangler.toml [vars] |
Builds Checkout’s success/cancel URLs and the trusted CORS origin. |
ENABLE_AUTOMATIC_TAX |
wrangler.toml [vars] |
Set to "true" only after configuring Stripe Tax in your account — otherwise leave "false". |
cart_worker_url (site’s _config.yml, not the Worker) |
Jekyll _config.yml |
Tells the frontend cart JS where this Worker lives. |
There’s no public/publishable Stripe key anywhere in this setup — since
Checkout is fully hosted by Stripe (the cart just redirects the browser to
the url the Worker returns), the frontend never touches Stripe.js or a
publishable key at all.
Webhook setup in Stripe
Stripe Dashboard → Developers → Webhooks → Add endpoint:
- Endpoint URL:
https://choose-arkansas-cart.YOUR-SUBDOMAIN.workers.dev/stripe-webhook - Events to send:
checkout.session.completed
After creating it, Stripe shows a signing secret (whsec_...) — that’s what
goes into STRIPE_WEBHOOK_SECRET.
Where fulfillment hooks in
src/index.js, inside the checkout.session.completed handler, has a
clearly marked TODO block right after the order is written to KV. That’s
the one place to add order-confirmation emails, a push to Airtable (matching
the pattern used elsewhere, e.g. the CRG Events CRM), or a call to a
shipping/fulfillment API. Orders are stored in KV under order:<session_id>
in the meantime — you can inspect them with:
npx wrangler kv key list --namespace-id=<your-namespace-id>
npx wrangler kv key get "order:cs_test_..." --namespace-id=<your-namespace-id>
Testing with Stripe test mode
- Make sure
STRIPE_SECRET_KEY/STRIPE_WEBHOOK_SECRETare your test mode values, and Price IDs in_data/products.ymlare test-mode Price IDs (they’ll start resolving once you complete the steps above). - To test the webhook locally before deploying:
stripe listen --forward-to localhost:8787/stripe-webhook(run
npm run devin another terminal first).stripe listenprints a temporary webhook secret — use that in.dev.varswhile testing locally. - Add a few products to the cart on the site, go to checkout, and use
Stripe’s test card:
4242 4242 4242 4242, any future expiry, any CVC, any ZIP. - Confirm:
- You land on
/order-success/with asession_idin the URL and the cart badge goes back to 0. wrangler tail(or the KV order record) shows the completed order.- Clicking “back” during Checkout (or using Stripe’s test “back” link)
returns you to the site with
?checkout=canceledand the cart still intact. - Re-sending the same webhook event from the Stripe Dashboard’s webhook logs (“Resend”) does not create a duplicate order record.
- You land on
Security notes
- The Stripe secret key and webhook secret only ever exist as Worker
secrets (
wrangler secret put) — never inwrangler.toml, never in.dev.vars(gitignored), never in the Jekyll site or its repo. - CORS is restricted to
SITE_URL, not*. - All pricing is resolved server-side from
price-map.json; the browser only ever sends product/variant identifiers and quantities.