The Paradox Process: Development

Role
Full-Stack Developer & Technical Director
When
June 2024 — present
Stack
  • Next.js
  • React
  • TypeScript
  • Stripe
  • Zoom
  • Neon
  • PostgreSQL
  • ComfyUI

The Paradox Process is a coaching company. When I started, the business ran on a stack of commercial SaaS — Keap for CRM and marketing automation, Thinkific for course delivery — stitched together and increasingly expensive and rigid. I rebuilt the whole thing as a single custom application: Next.js 16 (App Router, React 19.2, Turbopack), Postgres on Neon, payments through Stripe, live webinars through Zoom, and an offline-capable PWA. It has been in production since April 2026, serving real learners and real revenue.

The part I'm proudest of is also the part that was hardest to get right.

The rebrand

The Paradox Process website, the team's original version compared with the current designAfter — live today
Before — the old site
Before — the old siteAfter — live today

Drag the slider, or use arrow keys. The right side is the live site at paradoxprocess.org — this is not a mockup.

Look at the headline on both sides: it's the same promise. The message was never the problem — the team knew exactly who they were, and they built that first site themselves, far outside their comfort zone. It carried the business for years, and I admire it. My job wasn't to fix their vision. It was to give it the craft it deserved.

Two pigments — orange and blue — exist at rest: marks, rules, stamps. Never backgrounds.
Two signals — brighter twins of each — speak only in response: hover, focus, active.
Mixing pigment and signal is the number-one way a brand drifts; the system makes it hard to do by accident.

The site's imagery is its own system. Blog posts and courses had previously shipped with no thumbnails at all — and stock photography would have broken the brand's warmth. So I built a house style instead: in ComfyUI, I mixed LoRAs until the output had a consistent hand — warm, painterly, unmistakably one voice — then generated thumbnails for every existing blog post and course in the catalog. The illustrations in the captures below are that style.

The facilitator certification page on the live site
The facilitator path — four courses, native delivery.
The media and editorial section on the live site
The media library — same brand, different register.
The live classes page, generated by the evergreen class system
The classes page — the evergreen system's output. Enter a topic and a date; this page builds itself.
The live store page with Stripe checkout
The store — Stripe checkout, fulfilled by webhook.

The platform

A coaching company with a real method — and a digital presence the team had built themselves, well outside their comfort zones, that genuinely carried the business for years. But it had grown heavy to run: rented software everywhere, and a monthly ritual where a senior practitioner hand-assembled a class page, a checkout, an automation, a tag, and a batch of emails. The tools didn't help — Keap couldn't even send an email relative to a custom date. Hours of expert focus going to setup work a system should do.

That ritual became one evergreen class system: a human enters the topic, the date, and the price — the listing page, the Stripe checkout, the Zoom session, the reminder and replay emails, even the replay course all generate themselves. Reused on purpose, because hand-rewriting emails every month sounded personal in theory and produced typos in practice.

The deeper job was the data. Two source systems held everything that mattered: Keap had the contacts, consent state, sales opportunities, and years of interaction history. Thinkific had the course ownership — who owned what, and how far they'd gotten. I was replacing both, and I had also rebuilt every course from scratch as native courses in the new system. So the old catalog and the new one share no IDs. This was never a copy job; it was a reconciliation.

I built it as a lazy claim rather than a big-bang cutover. The first time a returning learner re-establishes access — through signup, Google sign-in, or a magic-link request — the system matches their old Thinkific records by email and grants them the new-catalog entitlement for each course they still had active access to. No maintenance window, no frozen business, no risk of a bad bulk run. The tradeoff I accepted is that this migration code lives in the app indefinitely instead of running once and being deleted — for a coaching business that can't afford downtime, that was the right trade.

The old-to-new course mapping is deliberately layered, because no single source was complete: an explicit replay-course mapping in the database comes first.
Behind it, a fallback derived from bundle metadata, then a facilitator-cohort mapping.
Finally, a hardcoded table for the four flagship courses — the ones that absolutely could not mis-map.
Every claim is idempotent and stamps the old record with the exact entitlement it produced — re-running on every sign-in is safe, and anything that doesn't map is recorded for repair rather than dropped silently.

One bug from this work is a good story on its own: granting an entitlement re-emitted a "granted" event even on a duplicate run, which double-enrolled learners into the welcome automation. The fix turned grant into a true upsert that returns early on duplicates — and it's why the lazy claim is safe to fire repeatedly. On the Keap side, contacts, tags, consent, opportunities, and full interaction history came over as first-class CRM records; campaign history is preserved as timeline events rather than resumed as live in-flight sequences.

Making it ship

Getting the app to build was never the hard part — getting it to deploy reliably was. The sharpest version of this: one masking failure hid a stack of latent ones underneath it.

The build's typecheck was silently running out of memory (exit 134) and skipping every downstream step — including the entire test suite, which is how a month of test drift accumulated unseen.
Database handles opened eagerly at import time, before any request existed to need them.
Secrets arrived as empty strings rather than absent — which defeated the test fixtures and failed 157 test files at once.
Playwright wedged in CI for 86 silent minutes, until the job moved into the official Playwright container.
The container introduced its own bug: a git "dubious ownership" error, because its UID didn't match the checkout.
The launch-gate had to be carved into a database-free slice after merchandising moved to Postgres.
Every Vercel preview deploy was structurally red: the build imports every route, which imports the env module, whose validation required Production-only Stripe secrets that previews don't have.

The seventh got its own fix: env validation split into a deferred tier — Stripe secrets validate lazily on first use outside production, and stay eager in production so a real misconfiguration still fails loudly at build. The lesson I kept: "works on my machine," "works in CI," and "works in a preview deploy" fail for completely different reasons, and the gaps between them are their own engineering problem.

One bug only Safari could see. The site uses Partial Prerendering, which renders a page's static shell at build time — and the framework bakes a single nonce into that cached shell. But every live request carries a fresh nonce in the Content-Security-Policy header, so the two never match. Per CSP Level 3, once a nonce is present on a directive the browser ignores 'unsafe-inline' entirely — there's no graceful fallback. Safari enforces this strictly and blocked the shell's inline styles, so public pages rendered unstyled for Safari users; Chrome and Firefox happened to be lenient about the exact same violation.

The fix was a scoped, deliberate tradeoff. For style-src-elem I dropped the nonce and accepted 'unsafe-inline' for inline styles only — script-src keeps strict per-request nonce enforcement, and external stylesheets are still gated by an allowlist. I evaluated the alternatives and rejected them with reasons: hash-based CSP is effectively webpack-only and we run Turbopack; disabling inline critical CSS traded a real first-paint regression for a marginal gain; a per-build stable nonce is no better than 'unsafe-inline' against anyone who can read page source. A nonce that's stale by construction protects nothing, so I stopped pretending it did — and kept the strict policy everywhere it still does its job. It's covered by a test so it can't silently regress.

Around those: Stripe for checkout and tiered pricing; Zoom webinars provisioned through server-to-server OAuth, with a boot-time guard that rejects the "me" host shortcut (it silently 404s under S2S, so a misconfigured value fails loudly at startup instead of at the first live session); Neon Postgres via the serverless driver and Drizzle; a Serwist service worker for offline support; and the customer-facing routes themselves — booking, a facilitator directory, free intro sessions, a store, and course delivery.

Shipping a real business turned out to be mostly the unglamorous parts: migrating data you can't afford to lose, debugging failures that only appear in environments you don't control, and making scoped, defensible tradeoffs when there's no clean answer. The features were the easy half.

The operator side of all of this — the page composer, the campaigns, the scheduling — grew into its own product: Anpa.