Friday, September 18, 2026

Building bidschoolrank.com: payments, a CDN, and a self-deploying pipeline in a day

A note from almost twenty years of blogging here: my first posts on this site, back in January 2008, were about comparing C# and VB syntax and wiring up custom configuration sections in XML. This one is about spinning up a payment-accepting web app, on a global CDN, behind a self-deploying pipeline, in under a day. The gap between those two sentences is the whole story of how software work has changed.

bidschoolrank.com

I built a small site called bidschoolrank.com. It is, admittedly, gimmicky: a school-ranking board where each contribution turns into rank points, one dollar to one point, no multipliers, and the all-time board never resets. But it was never really about the gimmick. It was an exercise in doing the three things that used to each be their own multi-week project: build a site, integrate payments, and stand up a real CI/CD pipeline. All of it came together in less than a day.

The stack

The whole thing is a small TypeScript monorepo with three packages:

  • Web — a Vite + React + TypeScript single-page app, served as static files.
  • API — a single AWS Lambda handler routing REST calls over a DynamoDB single-table model. One clever bit: the sort key encodes a zero-padded inverted score, so a single query returns schools highest-first with no full-table scan and no in-memory sort.
  • Infra — an AWS CDK v2 stack describing everything: a private S3 bucket, CloudFront in front of it with Origin Access Control and security headers, the DynamoDB table, an HTTP API Gateway, and the bundled Lambda.

The shape is the modern serverless default. The browser talks to CloudFront; static routes go to the private S3 bucket, and /api/* routes to API Gateway, then Lambda, then DynamoDB. No servers to patch, no service accounts to babysit. I could not help thinking back to 2015, when I stood up TeamCity on a Windows Server 2012 R2 box in AWS and spent real time just getting a service account permitted to Log on as a service. This time the infrastructure is code, and there is no box.

Payments: letting Stripe be the source of truth

Stripe was the part I most wanted to get right, because payments are where sloppy code turns into real money problems. The rule I held to: the web app never credits anything; only a signature-verified Stripe webhook does.

The flow:

  1. The app calls the API to create a Stripe Checkout Session for a contribution and hands the browser the redirect URL. That is all the front end can do — it cannot grant points.
  2. Stripe collects the payment and, when the session actually completes, calls back to a /stripe/webhook endpoint.
  3. The webhook handler verifies the Stripe signature before trusting a single byte. Then it credits the amount Stripe confirms was paid (the session's amount_total), not any number the client echoed back, and only when the payment status is actually paid.

Two details I am proud of, because they are exactly the things that bite people later:

  • Idempotency. Stripe re-delivers webhook events; it retries whenever your endpoint does not return a 2xx. So crediting is keyed on the Stripe event id — a "credit once" operation — and the endpoint always acknowledges with a 200 so Stripe stops retrying a message it already delivered.
  • Reversal-safe. The handler also listens for charge.refunded and charge.dispute.created, and reverses the corresponding credit. A refunded or disputed gift should not keep inflating a ranking. The PaymentIntent is stored so a later reversal can find what to undo.

None of that is exotic. But it is the difference between a demo and something you would let touch a real card, and it is the sort of correctness that Stripe's own primitives (signed webhooks, confirmed amounts, event ids) make achievable in an afternoon rather than a sprint.

The pipeline: push to main, and the rest happens

The part that would have been science fiction to me in 2008 is the delivery pipeline. School Rank deploys through a self-mutating AWS CDK Pipeline sourced from a Git branch:

  1. Synth — install, build, lint, test, and synthesize the CDK stack. This is the verification gate; if it fails, nothing ships.
  2. Test — deploy the full stack to a test.bidschoolrank.com environment wired to Stripe's sandbox keys, then publish the SPA and invalidate the CDN.
  3. Promote to Prod — a manual approval gate. It pauses and publishes to an SNS topic; a small notifier Lambda forwards the pending approval to Slack, carrying the approval token. I approve it from chat.
  4. Prod — the identical stack code path deploys to bidschoolrank.com with Stripe's live keys.

The elegant part is that test and prod are the same stack code, parameterized by an environment config — sandbox secrets versus live secrets, a test table versus the real one. There is one code path to reason about, not two that drift apart. After the pipeline exists, I stop running deploy commands by hand entirely: I push to the branch, the pipeline rebuilds itself and redeploys, and the only human step is clicking approve for production.

What actually changed in twenty years

Reading back through this blog, the throughline is not the languages. C# is still here; so is SQL. The throughline is how much ceremony has been absorbed by tools and platforms:

  • 2008: I was reducing the friction of application code — typed datasets, XML config, hand-mapped data readers.
  • 2015: I was reducing the friction of shipping the app — a build server on a VM I had to administer.
  • Today: the app, the CDN, the database, the payment integration, and the deployment pipeline are all just code in a repo, and most of the operational toil — certificates, scaling, retries, invalidation — is handled by the platform.

The hard problems did not disappear; they moved up the stack. Now the interesting work is correctness at the seams: idempotent webhooks, confirmed-amount crediting, reversal safety, keeping test and prod honest with each other. That is a better place to spend the effort than fighting a Windows service account.

Gimmicky project, sure. But it reminded me why I got excited about this work in the first place — and how far the ground has shifted under all of us.

This post was drafted with AI assistance and reviewed by the Eason Books team.