Skip to content

feat: add /donate page with Stripe embedded checkout and Cloudflare Worker backend#118

Closed
alexwolson wants to merge 33 commits into
mainfrom
samosas
Closed

feat: add /donate page with Stripe embedded checkout and Cloudflare Worker backend#118
alexwolson wants to merge 33 commits into
mainfrom
samosas

Conversation

@alexwolson

@alexwolson alexwolson commented May 5, 2026

Copy link
Copy Markdown
Collaborator

The below PR notes are generated by Claude as a result of the work I've done on this. Everything I'm adding here, I've personally tested end-to-end using Stripe's official testing protocols. Secure information is exclusively handled by Stripe, using our already-existing CTTO stripe account which is connected to our bank account directly. There is no opportunity for information leakage here, and I volunteer to make the first real donation in order to demonstrate that everything is set up properly.

Summary

  • Adds a /donate/ page to civictech.ca for the 2027 food funding campaign, with a goal of raising $3,500 CAD to cover samosas across 50 Tuesday meetups
  • Implements a Cloudflare Worker (ctt-donations.ctt-donations.workers.dev) with three endpoints: GET /total, POST /checkout, POST /webhook
  • Persists a live donation total in Cloudflare KV, displayed as a progress bar on the page
  • Stripe Embedded Checkout renders inline on the page — donors never leave civictech.ca and no card data touches our infrastructure
  • Two new GitHub Actions secrets required before this will build: STRIPE_PUBLIC_KEY and WORKER_URL

Architecture

flowchart LR
    Page["civictech.ca/donate/\nJekyll static page"]
    Worker["Cloudflare Worker"]
    KV["Cloudflare KV\nrunning total"]
    Stripe["Stripe\npayment + events"]

    Page -- "GET /total" --> Worker
    Page -- "POST /checkout" --> Worker
    Worker -- "clientSecret" --> Page
    Page -- "card details" --> Stripe
    Stripe -- "POST /webhook" --> Worker
    Worker -- "read/write total_cents" --> KV
Loading

Jekyll page (_pages/donate.html): campaign explainer, live progress bar, preset amount buttons ($10 / $25 / $50 / custom / Sponsor a Tuesday $65 / Sponsor a month $260), donate button, embedded checkout mount, and thank-you state on return from Stripe.

Cloudflare Worker (worker/): plain JS, no framework, deployed to Cloudflare's edge. Stripe secret key and webhook secret live in Cloudflare Worker secrets (never in the repository).

Cloudflare KV: stores a single key total_cents. Incremented on each checkout.session.completed webhook event.

Stripe: processes card payments via Embedded Checkout (ui_mode: embedded). Fires checkout.session.completed webhook on success. Automatic email receipts enabled. Currency: CAD.

Files changed

File What changed
_pages/donate.html New page: campaign copy, progress bar, amount selector, Stripe checkout integration
worker/src/index.js Worker entry point: GET /total, POST /checkout, POST /webhook handlers
worker/src/validate.js validateAmount() — validates donation amount (integer, $1–$3,500)
worker/src/webhook.js incrementTotal() and verifyStripeSignature() (HMAC-SHA256 via Web Crypto API)
worker/test/ Vitest unit tests for validate and webhook functions
worker/wrangler.toml Cloudflare Worker config with KV namespace binding
worker/package.json Worker dependencies (wrangler, vitest)
.github/workflows/pages.yml Adds STRIPE_PUBLIC_KEY and WORKER_URL secret injection at build time

Security

  • Card data is handled exclusively by Stripe — it never touches the Worker or the Jekyll page
  • Stripe webhook endpoint is protected by HMAC-SHA256 signature verification using STRIPE_WEBHOOK_SECRET (stored in Cloudflare Worker secrets)
  • STRIPE_SECRET_KEY lives in Cloudflare Worker secrets, never in the repository
  • STRIPE_PUBLIC_KEY is a publishable key — safe to embed in page JS
  • Cloudflare KV stores only a single integer (running total in cents) — no PII, no card data
  • CORS restricts Worker access to https://civictech.ca only in production

Before merging

Two GitHub Actions secrets must be added in repo Settings → Secrets → Actions:

Secret Description
STRIPE_PUBLIC_KEY Stripe publishable key (pk_live_... for production)
WORKER_URL https://ctt-donations.ctt-donations.workers.dev

Testing performed

  • Validated all three Worker endpoints locally and against the deployed Worker
  • Completed test payments using Stripe test card 4242 4242 4242 4242 across multiple preset amounts and a custom amount
  • Confirmed checkout.session.completed webhook fires, signature verification passes, and KV total increments correctly
  • Confirmed thank-you state renders on return from Stripe with ?session_id= in URL
  • Confirmed progress bar updates to reflect live total
  • Worker unit tests: 23 tests passing (validateAmount, incrementTotal, verifyStripeSignature)

Generated with Claude Code

alexwolson and others added 13 commits May 4, 2026 19:48
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove locally-added test keys from _config.yml
- Fix "Sponsor a month" amount: $270 → $260 ($65 × 4)
- Update explainer copy: clearer framing of what donations cover
- Remove em dashes from all copy
- Fix thank-you message to reflect food funding, not general operations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alexwolson
alexwolson requested a review from Copilot May 5, 2026 13:04
@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Pa11y Accessibility Report

All pages passed — 0 warning(s) (WCAG2AA)


Homepage

✅ No issues found.


Event

✅ No issues found.


Project

✅ No issues found.


Person

✅ No issues found.


Organization

✅ No issues found.


Venue

✅ No issues found.


Resource

✅ No issues found.


Tag

✅ No issues found.


Category

✅ No issues found.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new /donate/ page to the Jekyll site and a Cloudflare Worker backend to support Stripe Embedded Checkout plus a live donation progress total stored in Cloudflare KV.

Changes:

  • Adds Cloudflare Worker with GET /total, POST /checkout, and POST /webhook endpoints and supporting validation/helpers.
  • Adds a new /donate/ Jekyll page that displays a live progress bar and mounts Stripe Embedded Checkout inline.
  • Updates GitHub Pages workflow to inject Stripe publishable key and Worker URL into _config.yml at build time.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
worker/wrangler.toml Configures the Cloudflare Worker entrypoint and KV binding.
worker/vitest.config.js Sets up Vitest configuration for Worker unit tests.
worker/test/webhook.test.js Adds unit tests for KV total increment logic.
worker/test/validate.test.js Adds unit tests for donation amount validation.
worker/src/webhook.js Implements KV counter update and Stripe webhook signature verification.
worker/src/validate.js Implements donation amount validation rules.
worker/src/index.js Implements Worker routing + Stripe Checkout Session creation + webhook handler.
worker/package.json Adds Worker dev tooling dependencies and npm scripts.
worker/package-lock.json Locks Worker npm dependency tree.
worker/.gitignore Ignores Worker-local artifacts (node_modules, Wrangler state, local vars).
docs/superpowers/specs/2026-05-04-donation-campaign-design.md Adds design spec documenting the donation architecture and UX.
docs/superpowers/plans/2026-05-04-donation-campaign.md Adds implementation plan and operational/testing runbook.
.github/workflows/pages.yml Injects STRIPE_PUBLIC_KEY and WORKER_URL into Jekyll config during CI build.
_pages/donate.html Adds /donate/ page UI + Stripe Embedded Checkout client integration.
Files not reviewed (1)
  • worker/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread worker/package.json Outdated
Comment thread worker/src/webhook.js
Comment thread worker/src/webhook.js
Comment thread worker/src/webhook.js Outdated
Comment thread worker/src/webhook.js
Comment thread worker/src/index.js
Comment thread worker/src/index.js Outdated
Comment thread _pages/donate.html
alexwolson and others added 8 commits May 5, 2026 09:22
node-addon-api and node-gyp were listed as runtime deps but the Worker
uses no native addons. Removed them and regenerated the lockfile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced signatures.includes(computed) string comparison with
crypto.subtle.verify, which performs a constant-time comparison
internally and avoids timing side-channels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers valid signature, wrong signature, multiple v1 values (key
rotation), tampered body, wrong secret, and malformed headers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…victech.ca

Previously the fallback set Access-Control-Allow-Origin to civictech.ca
for every origin, which incorrectly told browsers all origins were
allowed. Now the header is only included for origins in the allowlist.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Check r.ok before parsing JSON so non-2xx responses fall through to
  the catch handler rather than producing NaN UI state
- Guard against missing/non-finite raised_cents
- Clamp rendered dollars to 3500 so the <progress> bar never exceeds max

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…erance

- Trim whitespace around comma-separated header fields
- Skip malformed fields (no '=' or empty value)
- Reject non-numeric timestamps
- Reject signatures older than 5 minutes or more than 5 minutes in the
  future (Stripe replay-attack mitigation)
- Update tests to use dynamic timestamps and cover new cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 10 comments.

Files not reviewed (1)
  • worker/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/workflows/pages.yml
Comment thread worker/src/validate.js
Comment thread worker/src/webhook.js Outdated
Comment thread worker/src/webhook.js Outdated
Comment thread worker/src/webhook.js
Comment thread worker/src/index.js Outdated
Comment thread worker/src/index.js Outdated
Comment thread _pages/donate.html Outdated
Comment thread _pages/donate.html
Comment thread _pages/donate.html Outdated
alexwolson and others added 9 commits May 5, 2026 10:24
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Returning 400 causes Stripe to retry for days with no benefit since
the payload won't change. Log the anomaly for manual reconciliation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.

Files not reviewed (1)
  • worker/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread worker/src/webhook.js
Comment thread worker/src/index.js
Comment thread worker/src/index.js
Comment thread .github/workflows/pages.yml
Comment thread worker/package.json
Comment thread _pages/donate.html

@gabesawhney gabesawhney left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good?

@Khasir

Khasir commented May 11, 2026

Copy link
Copy Markdown
Collaborator

I have 2 questions:

  1. I assume the new Cloudflare Workers fall under the free tier and won't accrue any costs?
  2. Does this change any part of the local testing & deployment process?

@alexwolson

Copy link
Copy Markdown
Collaborator Author

I have 2 questions:

  1. I assume the new Cloudflare Workers fall under the free tier and won't accrue any costs?
  2. Does this change any part of the local testing & deployment process?
  1. This is correct!
  2. Good question - the new code can be tested locally by providing test API keys from Stripe. For deployment, nothing changes really. I added new public secrets to the GitHub repo and that's all we need.

@Khasir Khasir left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll approve! @alexwolson for any changes/additions to testing do you think it makes sense to update the readme in another PR?

@alexwolson

Copy link
Copy Markdown
Collaborator Author

I'll approve! @alexwolson for any changes/additions to testing do you think it makes sense to update the readme in another PR?

Definitely!

@alexwolson alexwolson closed this May 19, 2026
@alexwolson

Copy link
Copy Markdown
Collaborator Author

Not adding for now as there are further discussions happening re. funding.

@alexwolson
alexwolson deleted the samosas branch July 21, 2026 22:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants