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
After — live today
Before — the old siteDrag 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.
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 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.
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 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.