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:

  1. Create Stripe Checkout Sessions (POST /create-checkout-session) — looks up trusted Stripe Price IDs from src/price-map.json for whatever {slug, size, color, qty} the cart sends. It never trusts a price the browser sends.
  2. 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:

  1. Create the Products/Prices in Stripe (test mode first).
  2. Paste each real price_... ID into the matching slot in _data/products.yml under that product’s price_ids: map.
  3. Regenerate the Worker’s trusted copy:
    python3 scripts/generate-price-map.py
    
  4. Redeploy the Worker: npm run deploy (from cloudflare-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:

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

  1. Make sure STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET are your test mode values, and Price IDs in _data/products.yml are test-mode Price IDs (they’ll start resolving once you complete the steps above).
  2. To test the webhook locally before deploying:
    stripe listen --forward-to localhost:8787/stripe-webhook
    

    (run npm run dev in another terminal first). stripe listen prints a temporary webhook secret — use that in .dev.vars while testing locally.

  3. 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.
  4. Confirm:
    • You land on /order-success/ with a session_id in 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=canceled and 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.

Security notes