Back to selected work

Principles of Pain Science

Chronic Pain & Fatigue Research Center · University of Michigan
Diagram of the two learner paths. After registering, a participant picks a path. On the certificate path they take a pre-assessment before any chapter, move through chapters that unlock in order and require passing each quiz, then take a final exam that needs 80% or more to pass, which earns the certificate. On the casual path every chapter is open from a table of contents, a chapter is finished by watching 80% of its video, the quiz is optional, and there is no final exam. A note says that on either path a chapter's quiz unlocks once 80% of its video is watched.

A learning platform for a clinical research center's chronic pain curriculum. Chapters unlock on watch time and quiz results, and the research team maintains all of the content themselves.

Role
Sole developer: architecture, build, deployment
Stack
Astro 6 · Cloudflare Workers · Cloudflare D1 · Drizzle ORM · Better Auth · Sanity v3 · Tailwind v4 · Alpine.js
Status
In development

What they needed

The Center wanted more than a static explainer site. They needed a self-paced curriculum where a participant's progress through each chapter means something. A chapter shouldn't unlock because someone scrolled past it. It should unlock once they've watched enough of the chapter video and passed the quiz, and that progress has to be saved per person so the team can report on it later.

Learners choose one of two paths. The certificate path starts with a pre-assessment, unlocks chapters in order as each quiz is passed, and ends with a final exam that needs 80% to earn the certificate. The casual path has no pre-assessment and no locked units, and a chapter counts as done once enough of its video has been watched, so the quiz is optional.

The people writing the curriculum are researchers and clinical staff, not developers. They needed to add and edit every chapter, quiz question, and piece of media themselves, without a deploy and without asking me first.

Architecture

Astro 6 runs in hybrid mode on Cloudflare Workers. Authenticated routes (the dashboard, chapter gating checks, progress endpoints) render on the server per request, and the informational pages around them stay static. Only the pages that need per-user logic pay for server rendering.

Progress lives in Cloudflare D1 through Drizzle ORM. A typed relational schema enforces the gating rules (video threshold met, quiz passed, chapter marked complete) in one place and makes the data queryable, which matters when progress may feed into participant tracking. Access uses Better Auth with a KV-based allowlist, so participation is provisioned per email and there is no open signup. The end-to-end tests use the same allowlist: they seed a known entry and reset a test user before each run, so the auth layer is never mocked.

The curriculum (chapters, quiz content, media) lives in Sanity v3, separate from the auth and progress code so the editing side stays simple. That only works if editors can actually use it, so I wrote a CMS training manual for research staff and a shorter developer guide, then built a NotebookLM notebook on both so the team can ask questions instead of searching a markdown file.

npx wrangler d1 migrations apply pops-db --local --config wrangler.json npm run test:e2e:kv-allowlist # seed a known allowlist entry npm run test:e2e:reset-user # reset the e2e test user npm run test:e2e # Playwright, against a real D1/KV-backed dev server

Constraints

The main constraint was testing an auth-and-progress flow without mocking it. Gating touches auth state, D1 data, and KV, so the end-to-end suite needs a repeatable way to reset a known user and re-seed the allowlist before every run. Those steps are npm scripts instead of setup notes. Vitest covers the gating rules at the logic level, and Playwright covers the full path a participant takes.

The other constraint was search. The platform needed on-site search across the curriculum without a separate hosted search service. Pagefind runs as its own step in the build pipeline before the Astro build, so search stays static and adds no runtime dependency.

  • Auth-gated chapter progression, enforced server-side
  • Typed progress schema via Drizzle + D1
  • Editor-owned curriculum, with training docs for non-developers
  • Vitest for logic, Playwright for e2e with scripted auth fixtures
Deploys via GitHub Actions to Cloudflare Workers. Not yet public. Currently in development for the Center.