## The premise
You have a Next.js project, an idea, and a weekend. Can you actually launch a paid digital store by Monday morning? I tried it. Here is the hour-by-hour timeline of what actually happens, plus the nine specific things that will eat your time if you do not know about them upfront.
## Hour 0: scope
You need: storefront, admin panel, checkout, post-payment fulfillment, transactional email, custom domain, SSL. Anything else can wait.
## Hour 1: Supabase project + migrations
Create the project, copy three keys, paste into .env.local. Run schema migrations. The non-obvious bit: the auto-create-profile trigger needs SET search_path = public, auth or signups silently 500. Search the SUPABASE issue tracker for handle_new_user errors and you will find a hundred people who hit this.
## Hour 2: Stripe sandbox + Checkout
Create a sandbox (not the legacy test mode), grab pk_test and sk_test, put them in env. Implement /api/checkout to create a Stripe Checkout Session. Hosted Checkout is dramatically simpler than Payment Element — let Stripe do the form work.
## Hour 3: Webhook handler
The biggest trap. Local dev needs `stripe listen --forward-to localhost:3001/api/webhooks/stripe` to get a whsec. Use that whsec for local dev only — production uses a different secret from a registered Dashboard endpoint. Mixing them up gives you 400 signature errors.
## Hour 4: Resend email
Free tier delivers ONLY to your registered Resend account email until you verify a sending domain. Three DNS records (SPF + DKIM + bounce MX) and 15 minutes of propagation later you can send to anyone. Add DMARC the same day or your emails will land in spam for the first month.
## Hour 5: Vercel deploy + custom domain
GitHub → Vercel → Import → add all env vars → Deploy. Add custom domain in Vercel. Vercel tells you a CNAME to add at your DNS provider. Five minutes of propagation, free Let's Encrypt cert auto-issued, you are live.
## Hour 6: production webhook
Go back to Stripe Dashboard, register the production webhook URL, copy the whsec, add to Vercel env, redeploy. Now paid orders flip to paid in your DB.
## Hour 7: smoke test
Buy your own product. Pay with 4242 4242 4242 4242. If everything is wired right, you get the order email at the email you typed at checkout, the order shows paid in admin, and the buyer sees the right confirmation page.
## What ate the rest of the time
The nine specific gotchas that cost me hours:
1. The handle_new_user trigger search_path issue (45 min)
2. Trailing whitespace in a fulfillment_target field producing silent GitHub 404s (30 min)
3. SDKs that throw at module load time when env vars are missing — broke the Vercel build until I refactored them to lazy-init (40 min)
4. RLS blocking guest checkout writes — solved by routing service-role for the writes (20 min)
5. useSearchParams in a page that prerenders — Next 16 requires a Suspense boundary (10 min)
6. Resend free-tier delivery limits — 30 minutes wondering why my test emails were not arriving (yes I had the API key right; no it does not deliver to addresses other than your own until you verify a domain)
7. CSP headers blocking Stripe scripts on the deploy — relaxed CSP for ship, then went back later with strict-nonce (1 hour)
8. Wrong Stripe sandbox / wrong API key during webhook setup (15 min)
9. Forgetting to update STRIPE_WEBHOOK_SECRET in Vercel after registering the production endpoint (15 min — and meanwhile every paid order was stuck on pending in the DB)
That is roughly four hours of pure pitfalls on top of seven hours of legitimate work. Eleven hours total — about the length of a long Saturday. Without the gotchas it is genuinely a one-day project.
## What I built afterwards to never debug those again
Steep: the codebase I wished I could have started from. Every gotcha above is now either fixed in the code, documented in TROUBLESHOOTING.md, or both.