Payment Flow
Stripe checkout integration: PaymentIntent creation, webhook processing with signature verification, library assignment on purchase completion, and debug-mode mock payments.
All payments are one-time book purchases. There is no subscription or recurring billing.
Flow
User Frontend Stripe Supabase
| | | |
| 1. Click Buy | | |
|---------------------->| | |
| | 2. Create PaymentIntent| |
| |------------------------>| |
| | 3. client_secret | |
| |<------------------------| |
| | 4. Render | |
| | Stripe Elements | |
| 5. Enter card | | |
|---------------------->| | |
| | 6. Confirm card | |
| |------------------------>| |
| | 7. Result | |
| |<------------------------| |
| 8. Show success | | |
|<----------------------| | |
| | | 9. Webhook fires |
| | |------------------------>|
| | | 10. Insert purchase + |
| | | add to library |PaymentIntent Creation
When the user clicks "Buy" on a book detail page, the frontend calls POST /api/payments/create-intent with { bookId }.
The API route:
- Authenticates the user via
auth(). - Fetches the book's price from the
bookstable. - Creates a Stripe PaymentIntent with
amount,currency, andmetadata: { bookId, userId }. - Returns
{ clientSecret }to the frontend.
The frontend then renders a Stripe Elements <CardElement> and calls stripe.confirmCardPayment(clientSecret). On confirmation, the user is shown a success page.
Webhook Processing
Stripe sends webhooks to POST /api/payments/webhook. The endpoint:
- Reads the raw body and verifies the Stripe signature using the webhook secret from the
STRIPE_WEBHOOK_SECRETenvironment variable. - Handles
checkout.session.completed:- Extracts
bookIdanduserIdfrommetadata. - Inserts a row into the
purchasestable with the Stripe session ID, amount, and status. - Inserts a row into
user_librarygranting the user access to the book. - Returns a
200 OKto Stripe.
- Extracts
Failed or duplicate webhooks are handled idempotently: the purchases table has a unique constraint on the Stripe session ID, so repeated webhook deliveries do not create duplicate library entries.
Error Handling
- Payment failure: Stripe Elements handles card errors (insufficient funds, expired card, etc.) client-side. The user sees the error on the checkout form and can retry.
- Webhook failure: Stripe retries the webhook up to three times. If all retries fail, the user's purchased book will not appear in their library - an admin can manually add it using the Stripe dashboard session ID.
- Network error: If the
create-intentcall fails, the user sees a generic error and can retry. - Book out of stock: The API route checks if the book exists before creating the intent. If the book does not exist or has no price, it returns a 400 error.
Debug Mode
When NEXT_PUBLIC_DEBUG_MODE=true:
- The checkout page shows a mock Stripe Elements form instead of a real one.
POST /api/payments/create-intentreturns a mockclientSecret.- The webhook endpoint is not hit - the frontend simulates a successful payment by directly calling the library assignment logic.
This allows payment development and testing without a real Stripe account.
File Storage
Supabase Storage with two buckets (public/private), the book upload pipeline, file validation, and signed URL generation.
Data Model
TypeScript type system, core entities (Book, Character, Session), Supabase table mapping, snake_case/camelCase conversion pattern, and the type modules that mirror the DB schema.