Production-ready Next.js 15 marketing and sales site for the Texas Hold'em Gym desktop app.
- Framework: Next.js 15 (App Router, TypeScript)
- Styles: Tailwind CSS — dark poker/casino theme
- Payments: Stripe Checkout (one-time $79)
- Email: Resend (transactional download links)
- Database: PostgreSQL + Prisma (purchase records, analytics events)
- Downloads: AWS S3 + CloudFront CDN
- Admin (
/admin): visits & click analytics, Stripe status, refunds
src/
├── app/
│ ├── page.tsx # Landing page
│ ├── layout.tsx # Root layout (Nav + Footer)
│ ├── globals.css
│ ├── features/page.tsx
│ ├── pricing/page.tsx
│ ├── buy/page.tsx # Buy button page
│ ├── success/page.tsx # Post-payment installer links (Windows + macOS)
│ ├── privacy/page.tsx
│ ├── terms/page.tsx
│ ├── contact/page.tsx
│ └── api/
│ ├── checkout/route.ts
│ ├── stripe/webhook/route.ts
│ ├── analytics/track/route.ts
│ └── admin/ # login, logout, stats, purchases, refund, stripe-config
├── admin/ # Protected UI (JWT cookie)
├── components/
│ ├── SiteChrome.tsx # Nav/Footer + analytics (skips /admin)
│ ├── AnalyticsTracker.tsx
│ ├── Nav.tsx
│ ├── Footer.tsx
│ ├── PlatformIcons.tsx # Windows / macOS SVG marks
│ └── BuyButton.tsx
└── lib/
├── stripe.ts
├── prisma.ts
├── admin-auth.ts
├── admin-api.ts
├── email.ts
└── downloads.ts
middleware.ts # Protects /admin
prisma/
└── schema.prisma
public/
├── icons/ # windows.svg, macos.svg (used in transactional email)
└── screenshots/ # App screenshots used on site
cd website
npm installcp .env.example .env.localFill in all values in .env.local (see comments in .env.example).
Admin panel: set ADMIN_JWT_SECRET (≥16 random characters) and ADMIN_PASSWORD. Open /admin, sign in, then use Visits & clicks, Purchases & refunds, and Stripe.
# Push schema to your Postgres database
npm run db:push
# Or run migrations (production)
npm run db:migrateGenerate the Prisma client:
npm run db:generate- Create a Stripe account at stripe.com
- Copy your Secret Key from the Stripe dashboard
- Set up a webhook endpoint pointing to
https://your-domain.com/api/stripe/webhook - Listen for the event:
checkout.session.completed - Copy the Webhook Signing Secret to
STRIPE_WEBHOOK_SECRET
- Create an account at resend.com
- Verify your sending domain
- Create an API key and set
RESEND_API_KEY - Set
FROM_EMAILto a verified sender address
Upload the Windows and macOS builds to your S3 bucket:
s3://your-bucket/downloads/texas-holdem-gym-windows.exe
s3://your-bucket/downloads/texas-holdem-gym-mac.dmg
Then set NEXT_PUBLIC_DOWNLOAD_BASE_URL to your CloudFront distribution URL.
npm run devVisit http://localhost:3000.
To test Stripe webhooks locally, use the Stripe CLI:
stripe listen --forward-to localhost:3000/api/stripe/webhookvercel --prodAdd all environment variables in the Vercel dashboard under Settings → Environment Variables.
npm run build
npm startSet all environment variables in your platform's config panel.
- Purchase — email, Stripe session id, optional payment intent id, amount, refund fields (
refundedAt,stripeRefundId). - AnalyticsEvent —
PAGE_VIEW/CLICK, name, path, referrer, user agent, IP, optional JSON metadata.
After pulling changes, run npm run db:push (or migrate) so new columns/tables exist.
- User clicks Buy Now
POST /api/checkoutcreates a Stripe Checkout session- User redirected to Stripe-hosted checkout page
- After payment: redirect to
/success?session_id=... - Stripe fires
checkout.session.completedwebhook to/api/stripe/webhook - Webhook stores purchase in DB and sends installer links email via Resend
/successpage shows Windows and macOS installer buttons immediately (no server-side validation required — user is already redirected there)
/download redirects to /buy (legacy URLs).
| Thing to change | Where |
|---|---|
| Price | src/lib/stripe.ts → PRODUCT_PRICE_CENTS |
| Product name | src/lib/stripe.ts → PRODUCT_NAME |
| Installer file names (S3 paths) | src/lib/downloads.ts |
| Email copy | src/lib/email.ts |
| Brand colours | tailwind.config.ts → colors.gold |
| Nav links | src/components/Nav.tsx |
| FAQ answers | src/app/page.tsx → FAQS array |