Skip to content
achacon.dev
All work

2026

Storefront behind a licence check

A headless storefront where the catalogue, pricing and checkout do not exist until the visitor is verified as a licensed practitioner.

Role
Sole engineer: front end, auth perimeter, and verification
Stack
TypeScriptNext.jsReactTailwindWooCommerce REST & Store APIJWTUpstashSentry
  1. Trigger: 01RequestAny route, signed in or not
  2. Process: 02Deny by defaultNothing is public until you declare it
  3. Process: 03Licence checkedServer-side, against the registry
  4. Store: 04Claim on the tokenVerified state carried, not re-asked
  5. Output: 05Catalogue servedPrices and checkout unlock together
Three states, and the catalogue only exists in the third.

The problem

These products could only be sold to licensed practitioners. A tick-box saying I am a physician verifies nothing, and a catalogue hidden behind a login sits one misconfigured route away from being public.

What I built

  • The perimeter denies by default. A route stays private unless someone declares it public, so forgetting to protect a new page locks it rather than exposes it.
  • Three states instead of two: public, signed in but unverified, and verified. Catalogue, prices and checkout unlock together on the third, and the session token carries that state as a claim so nothing re-derives it per request.
  • Verification runs server-side against the federal NPI registry. The check never touches the browser, so reading the network tab gets you nowhere.
  • WordPress keeps the commerce and stops rendering pages. The front end reads the catalogue over the REST API and drives cart and checkout over the Store API, and one module is the only thing in the codebase allowed to talk to it.
  • Rate limiting, error tracking, product analytics and structured logging went in from the start. A gated store with no telemetry leaves you unable to tell a bug from an attack.
  • Environment variables validate on first import and throw on a placeholder, so a misconfigured deploy fails at boot instead of at checkout.