I can turn an existing operator-run business into production software without flattening the business rules.
GIFTED HQ + Academics
Two live sites for one business: HQ runs brand, coaching, commerce, and operations; Academics runs courses, quizzes, payments, and access.
Payments, refunds, course access, cross-domain auth, product sync, and support flows all fail in different ways.
Two live surfaces, shared Clerk identity, Stripe webhook settlement, Sanity-to-Stripe sync, Postgres access truth, and product screenshots.
HQ - store, coaching, consults, and contact.
Academics - catalog, courses, lessons, and quizzes.
gifted-hq.com
Marketing, coaching commerce, store, partner portals, and operator tools.
- Brand site + team profiles
- Store, subscriptions, partner portals
- Coach products, templates, affiliates
- Sanity Studio + Stripe product sync
gifted-academics.com
Certification courses, checkout, access control, progress, and admin.
- Course catalog + lesson player
- Quizzes, progress, enrollments
- Payment plans + section unlocks
- Admin orders, discounts, SEO tooling
Editable business content, coach profiles, merch, training templates, affiliates.
Courses, purchase options, enrollments, quizzes, lesson progress, webhook dedupe.
Two live websites, one business. HQ runs the public brand, coaching, store, partner portals, and operator tools. Academics runs courses, quizzes, progress, payment plans, and admin. Customers still get one account and one billing relationship.
Why it exists
GIFTED was already a real business. The old digital footprint had the usual founder-operator problem: commerce in one place, courses in another, content edits somewhere else, and support stitched together with memory, spreadsheets, and inbox search.
The rebuild had to be an operating system, not a nicer landing page: product setup, checkout, fulfillment, course access, refunds, payment plans, video, discounts, and admin that could handle live customers without babysitting.
What made it hard
Live business software punishes optimistic code. Stripe can retry a webhook. A customer can refresh a success page. A payment plan can renew before a support ticket is closed. A refund should revoke access without corrupting order history. A course user may start on Academics and authenticate through HQ.
I built around those failure modes. HQ uses Sanity for editable business content and syncs publish events into Stripe. Academics uses Postgres for access truth. Webhooks are the settlement layer, not a callback afterthought.
HQ commerce loop
- 01 Publish
Sanity product, template, coach, affiliate, or store item
- 02 Sync
Webhook resolves Stripe product and price IDs, then writes them back
- 03 Checkout
Customer pays through Stripe with Clerk identity attached
- 04 Fulfill
Webhook sends email, updates metadata, inventory, Shippo, or downloads
Academics access loop
- 01 Author
Course sections, lessons, videos, documents, quizzes, purchase options
- 02 Sell
Stripe Checkout carries course, user, option, and attribution metadata
- 03 Enroll
WebhookEvent dedupe, then Enrollment upsert in Postgres
- 04 Unlock
Lesson access follows enrollment status and payment-plan progress
Three trade-offs worth naming.
The short version: choice, reason, cost.
Two apps, one shared identity
HQ and Academics are separate Next.js apps on separate domains. They share one Clerk user pool; Academics is the satellite and HQ is the auth home.
Coaching, store, and course access fail in different ways. Splitting the apps keeps those risks separate while customers still use one account.
Auth setup is fussier: satellite DNS, redirect origins, two Vercel envs, two webhook surfaces, and support copy for cross-domain sign-in.
Stripe events are the ledger, not a side effect
Stripe webhooks change state. HQ locks checkout with Upstash Redis and stamps metadata. Academics stores processed event IDs before enrollments, payment-plan updates, refunds, or receipts.
Retries are normal in a live business. Payments, refunds, fulfillment, and access have to survive them without crediting the wrong thing twice.
Every handler needs idempotency, retry behavior, and a recovery path after Stripe has already accepted the charge.
Use the right content store for each surface
HQ uses Sanity for merch, coaching products, templates, profiles, affiliates, and marketing content. Academics uses Postgres for courses, lessons, quizzes, enrollments, progress, and payment-plan unlocks.
HQ needs editable business content. Academics needs relational guarantees. Forcing both into one CMS would make the course platform worse.
Operators need to know the owner of each truth: Sanity, Stripe, Postgres, Clerk, or Bunny. Clean boundaries come with training overhead.
The stack.
- Next.js 16 App Router
- React 19
- TypeScript 5
- Tailwind 4
- Clerk · primary + satellite
- Stripe Checkout
- Stripe webhooks
- Sanity Studio
- Postgres · Neon
- Bunny CDN
- Resend
- Shippo
- Upstash Redis
- Sentry
- Vercel
- Claude Code