"The world is not short of problems. It is short of governed minds."
A full-stack e-commerce storefront for the book Think Like a King by Michael King — built with Next.js 16, Stripe, Prisma, Resend, and Lulu print-on-demand. Customers can purchase a token-secured digital download or a premium hardcover edition shipped globally.
- Overview
- Tech Stack
- Project Structure
- How It Works
- Environment Variables
- Database
- Getting Started
- API Reference
- Email Templates
- Deployment Notes
The site is a single-page storefront with a modal-based checkout. There are two purchasable editions:
| Edition | Price | Fulfillment |
|---|---|---|
| E-Book | $9.99 (configurable) | Token-secured download link emailed instantly |
| Hardcover | $24.99 (configurable) | Print-on-demand via Lulu, shipped globally |
Payment is handled entirely by Stripe Checkout. After a successful payment, the success page polls the order status API which verifies the session directly with Stripe as a fallback — meaning the flow works in local development without a live webhook tunnel.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript 5 |
| Styling | Tailwind CSS v4 |
| Database | PostgreSQL via Prisma 7 + @prisma/adapter-pg |
| Payments | Stripe (Checkout Sessions, Webhooks) |
| Resend | |
| Print-on-Demand | Lulu Direct API |
| Fonts | Germania One, Nunito (Google Fonts) |
src/
├── app/
│ ├── page.tsx # Landing page / storefront
│ ├── layout.tsx # Root layout, fonts, metadata
│ ├── globals.css # Global styles
│ ├── checkout/
│ │ ├── success/page.tsx # Post-payment success + polling UI
│ │ └── cancel/page.tsx # Cancelled checkout page
│ └── api/
│ ├── checkout/
│ │ ├── ebook/route.ts # POST — create Stripe session (e-book)
│ │ └── hardcopy/route.ts # POST — create Stripe session (hardcopy)
│ ├── order/
│ │ └── status/route.ts # GET — poll order status + fallback fulfillment
│ ├── download/
│ │ └── [token]/route.ts # GET — validate token, redirect to file
│ └── webhooks/
│ ├── stripe/route.ts # POST — Stripe checkout.session.completed
│ └── lulu/route.ts # POST — Lulu LINE_ITEM_SHIPPED
├── components/
│ └── CheckoutModal.tsx # Two-step checkout modal (edition select → details)
└── lib/
├── prisma.ts # Prisma client singleton (pg adapter)
├── stripe.ts # Stripe client
├── resend.ts # Resend client
└── emails.ts # HTML email templates (ebook, hardcopy, shipped)
prisma/
├── schema.prisma # Order, ShippingAddress, enums
└── migrations/ # Migration history
- Customer clicks Buy the Book anywhere on the landing page.
- The
CheckoutModalopens with a two-step flow:- Step 1 — Choose edition (E-Book or Hardcover). A 1.5s transition plays before advancing.
- Step 2 — Enter name, email, and (for hardcopy) a shipping address.
- On submit, the modal calls either
POST /api/checkout/ebookorPOST /api/checkout/hardcopy. - The API creates a Stripe Checkout Session using
price_datawith the amount read from env — no pre-created Stripe products required. - The customer is redirected to Stripe's hosted checkout page.
- On completion, Stripe redirects to
/checkout/success?session_id=cs_....
The success page polls GET /api/order/status?session_id=... every 1.5 seconds (up to 20 attempts / 30 seconds).
The status endpoint uses a two-path strategy:
1. Check DB for existing order → found → return COMPLETE
↓
not found
↓
2. Retrieve session from Stripe → payment_status !== "paid" → return PENDING
↓
payment confirmed
↓
3. Create order in DB + send email → return COMPLETE
This means the flow works in two scenarios:
- Production — Stripe webhook fires first, creates the order, DB lookup hits immediately.
- Local dev — Webhook can't reach localhost, so the status endpoint creates the order itself on the first confirmed poll.
E-book downloads use a UUID token stored on the order record.
- Token is generated at order creation time and expires after 48 hours.
GET /api/download/[token]validates the token, checks expiry, marksdownloadedAton first access, then issues a302redirect toEBOOK_FILE_URL.- In production,
EBOOK_FILE_URLshould be a short-lived signed URL from S3 or Vercel Blob — not a permanent public link.
Stripe (POST /api/webhooks/stripe)
- Verifies the
stripe-signatureheader usingSTRIPE_WEBHOOK_SECRET. - Only handles
checkout.session.completed. - Routes to
handleEbookorhandleHardcopybased onmetadata.productType. - Creates the order record and sends the appropriate confirmation email.
Lulu (POST /api/webhooks/lulu)
- Verifies the
x-lulu-signatureheader using HMAC-SHA256 andLULU_WEBHOOK_SECRET. - Only handles
LINE_ITEM_SHIPPED. - Finds the order by
luluOrderId, updates status toSHIPPED, saves tracking info, and sends the shipping notification email.
Copy .env and fill in the values. All variables are required unless marked optional.
# ── Database ──────────────────────────────────────────────────────────────────
DATABASE_URL="postgresql://user:password@host:5432/dbname?schema=public"
# ── App ───────────────────────────────────────────────────────────────────────
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
# ── Stripe ────────────────────────────────────────────────────────────────────
# https://dashboard.stripe.com/apikeys
STRIPE_SECRET_KEY="sk_test_..."
# https://dashboard.stripe.com/webhooks — add endpoint: /api/webhooks/stripe
# Events to listen for: checkout.session.completed
STRIPE_WEBHOOK_SECRET="whsec_..."
# ── Pricing (in cents — no Stripe products needed) ────────────────────────────
EBOOK_PRICE_CENTS="999" # $9.99
HARDCOPY_PRICE_CENTS="2499" # $24.99
# ── Resend ────────────────────────────────────────────────────────────────────
# https://resend.com/api-keys
RESEND_API_KEY="re_..."
# Must be a verified sender domain in Resend
EMAIL_FROM="orders@yourdomain.com"
# ── E-Book File ───────────────────────────────────────────────────────────────
# Direct URL or signed URL to the PDF/EPUB file
EBOOK_FILE_URL="https://..."
# ── Lulu (Print-on-Demand) ────────────────────────────────────────────────────
# https://developers.lulu.com
LULU_API_KEY="..."
LULU_WEBHOOK_SECRET="..."The schema has two models:
Order — one record per completed purchase.
| Field | Type | Notes |
|---|---|---|
id |
String (cuid) |
Primary key |
email |
String |
Customer email |
name |
String |
Customer name |
type |
ProductType |
EBOOK or HARDCOPY |
stripeSessionId |
String (unique) |
Stripe Checkout Session ID |
stripePaymentId |
String? |
Stripe PaymentIntent ID |
status |
OrderStatus |
PENDING → PAID → PROCESSING → SHIPPED |
downloadToken |
String? (unique) |
UUID for e-book download |
downloadExpiresAt |
DateTime? |
48h from order creation |
downloadedAt |
DateTime? |
First download timestamp |
luluOrderId |
String? |
Lulu order reference |
trackingNumber |
String? |
Courier tracking number |
trackingUrl |
String? |
Courier tracking link |
shippedAt |
DateTime? |
Timestamp when shipped |
ShippingAddress — one-to-one with Order, hardcopy only.
| Field | Type |
|---|---|
line1 |
String |
line2 |
String? |
city |
String |
state |
String? |
postalCode |
String |
country |
String (ISO 3166-1 alpha-2) |
Run migrations:
npx prisma migrate devOpen Prisma Studio:
npx prisma studioPrerequisites: Node.js 20+, PostgreSQL (local or hosted)
# 1. Install dependencies
npm install
# 2. Set up environment variables
# Edit .env with your actual keys (see Environment Variables above)
# 3. Run database migrations
npx prisma migrate dev
# 4. Start the development server
npm run devThe app will be at http://localhost:3000.
Testing payments locally:
Use Stripe's test card 4242 4242 4242 4242 with any future expiry and any CVC. The order status endpoint will verify the payment directly with Stripe after redirect — no webhook tunnel needed in development.
For webhook testing in dev, use the Stripe CLI:
stripe listen --forward-to localhost:3000/api/webhooks/stripeCreates a Stripe Checkout Session for the e-book.
Request body:
{ "name": "John Doe", "email": "john@example.com" }Response:
{ "url": "https://checkout.stripe.com/..." }Creates a Stripe Checkout Session for the hardcover edition.
Request body:
{
"name": "John Doe",
"email": "john@example.com",
"address": {
"line1": "123 Main St",
"line2": "Apt 4B",
"city": "New York",
"state": "NY",
"postalCode": "10001",
"country": "US"
}
}Response:
{ "url": "https://checkout.stripe.com/..." }Polls order status after checkout. Creates the order as a fallback if the webhook hasn't fired yet.
Response (pending):
{ "status": "PENDING" }Response (complete):
{
"status": "COMPLETE",
"type": "EBOOK",
"name": "John Doe",
"email": "john@example.com",
"downloadToken": "uuid-here-or-null"
}Validates a download token and redirects to the e-book file.
| Status | Meaning |
|---|---|
302 |
Valid token — redirects to file |
404 |
Token not found |
410 |
Token expired |
503 |
EBOOK_FILE_URL not configured |
Stripe webhook endpoint. Requires stripe-signature header.
Handles: checkout.session.completed
Lulu webhook endpoint. Requires x-lulu-signature header (HMAC-SHA256).
Handles: LINE_ITEM_SHIPPED
All emails are plain HTML built in src/lib/emails.ts with no external template engine. They share a dark-themed base layout matching the site's aesthetic — dark background (#1a1714), gold accents (#c9a84c), serif typography.
| Template | Trigger | Content |
|---|---|---|
ebookDownloadEmail |
E-book order confirmed | Secure download button + fallback link, 48h expiry notice |
hardcopyConfirmationEmail |
Hardcopy order confirmed | Formatted shipping address, estimated delivery window |
hardcopyShippedEmail |
Lulu LINE_ITEM_SHIPPED webhook |
Tracking number + carrier tracking button |
- Set
NEXT_PUBLIC_SITE_URLto your production domain (e.g.https://thinklikeaking.com). - Register
/api/webhooks/stripein the Stripe dashboard under Developers → Webhooks. Subscribe tocheckout.session.completedonly. - Register
/api/webhooks/luluin the Lulu developer dashboard. SetLULU_WEBHOOK_SECRETto match. EBOOK_FILE_URLshould point to a protected file URL. For Vercel Blob or S3, generate a short-lived signed URL at download time rather than storing a permanent public link.- The Prisma adapter uses the
pgconnection pool directly — ensureDATABASE_URLpoints to a connection pooler (e.g. PgBouncer or Neon's pooled endpoint) in production to avoid exhausting connections. - Prices are controlled entirely via
EBOOK_PRICE_CENTSandHARDCOPY_PRICE_CENTSin env. No Stripe dashboard products are required.
© Think Like a King — Revere & Regent Press