SaborCup
Specialty-coffee store — Stripe Billing lifecycle, gift cards, and a 24-model Postgres schema

Full-stack specialty-coffee store (Stripe test mode) — Stripe Billing subscriptions with pause, skip, and plan changes, webhook-minted gift cards, merch, PDF invoices, and a full admin console.
Problem
One-time checkout is the easier half of e-commerce. Subscriptions add lifecycle state — pauses, skips, plan changes, failed payments, dunning — that has to stay correct across webhooks, scheduled jobs, and user actions, and most portfolio projects avoid that complexity entirely.
Goal
Build a complete subscription-commerce store — Stripe Checkout for one-time orders and Stripe Billing for recurring plans — with the operational layer real stores need: webhook-driven state, scheduled jobs with recorded outcomes, transactional email, and separate customer and admin consoles. Then prove it with tests.
What I Built
A fictional specialty-coffee brand (labeled as such on every page; all payments in Stripe test mode). 10 seeded coffees and 15 merch items share one cart, checkout, review, and favorites flow. Three subscription plans ($14/$28/$48 per month) sit alongside per-coffee subscriptions with biweekly-to-monthly cadence changes. Everything after the first charge is the hard part — lifecycle controls, proration, and dunning — and that is where most of the schema and the test surface went.
Key Features
- Stripe Checkout + Stripe Tax for orders, Stripe Billing for subscriptions — prices, discounts, and gift-card balances re-validated server-side
- Subscription lifecycle: pause, skip-next-delivery, next-cycle plan changes, cancel/reactivate, and per-coffee subscriptions with cadence switching
- Dunning — invoice.payment_failed flags the subscription, emails the customer, and surfaces admin triage
- Gift cards minted by webhook on purchase, redeemed by code at checkout, balance restored on cancellation
- Volume bundles that stay off ineligible lines — Stripe percent_off coupon for coffee-only carts, per-unit price baking for mixed carts
- 24-model Postgres schema — dual-nullable product FKs share one commerce flow across coffee and merch; denormalized order items survive product deletion; unique Stripe IDs as a DB-level double-refund backstop
- 59 pages across four route groups, with scoped error and loading boundaries on the storefront, account, and admin shells
- One-click demo sign-in at /demo for customer and admin roles — token-gated, fail-closed nightly reset, demo admins fenced off from real roles and secrets
- Scheduled jobs with CronRun observability — daily renewal reminders (idempotency-keyed), cross-instance claims and outcomes recorded in the database
- Transactional email on one shared template shell via Resend, with unit-tested templates and a dev preview route
- PDF invoices rendered server-side from the account dashboard
- 59 Vitest test files (including a dedicated webhook-idempotency suite) + 4 Playwright E2E specs — hermetic against a guarded database locally, ephemeral Postgres in CI via GitHub Actions
Challenges
- Webhooks as the source of truth — checkout, billing, gift-card minting, and dunning all settle through Stripe events, so state stays correct even if the user closes the tab mid-checkout
- One commerce flow for two product types — dual-nullable FKs on order items, favorites, and reviews avoid duplicating cart and checkout logic across coffee and merch
- Mixed-cart bundle discounts — a single Stripe coupon over-discounts carts containing merch, so mixed carts bake the discount into per-unit prices instead
- Cron reliability on a platform with 1-hour logs and no retries — a CronRun model makes the database the evidence for what ran, claimed, and completed
- Keeping a public demo safe — token-gated demo auth, fail-closed reset guards, and demo admins that cannot create real admins or reach production secrets
Outcome
Live on Vercel in Stripe test mode with one-click demo accounts for both roles. 644 tests across 59 Vitest files and 4 Playwright E2E specs, run in CI against an ephemeral Postgres. Fictional brand, clearly labeled — nothing is for sale and no orders are fulfilled.
Next Steps
- Wire the persisted subscriber-discount and skip-limit policies into an actual pricing engine (fields exist, engine does not yet)
- Hook admin notification preferences to a delivery provider