# Les 17 — Huiswerk ## Webhook + Resend email + retry + APIS.md **Deadline:** Voor Les 18 — Supabase Auth + RLS **Inleveren:** GitHub repo + `APIS.md` in root --- ## Doel Bouw je Stripe-demo uit naar een echte productie-flow: betaling triggert webhook → database update → welkomstmail. Plus retry-logic en eindopdracht-reflectie. --- ## Onderdeel A — Webhook handler met signature verify (verplicht) ### Stripe CLI installeren ```bash # macOS brew install stripe/stripe-cli/stripe # of via https://docs.stripe.com/stripe-cli stripe login ``` ### Lokaal webhook luisteren ```bash stripe listen --forward-to localhost:3000/api/webhook/stripe ``` Output: `Ready! Your webhook signing secret is whsec_xxx`. Kopieer naar `.env.local`: ``` STRIPE_WEBHOOK_SECRET=whsec_xxx ``` ### Webhook endpoint `app/api/webhook/stripe/route.ts`: ```typescript import { headers } from "next/headers"; import Stripe from "stripe"; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); export async function POST(req: Request) { const body = await req.text(); const sig = (await headers()).get("stripe-signature")!; let event: Stripe.Event; try { event = stripe.webhooks.constructEvent( body, sig, process.env.STRIPE_WEBHOOK_SECRET! ); } catch (err: any) { return new Response(`Webhook Error: ${err.message}`, { status: 400 }); } // Idempotency check // const existing = await checkProcessedEvent(event.id); // if (existing) return Response.json({ received: true }); console.log("Stripe event:", event.type); if (event.type === "checkout.session.completed") { const session = event.data.object; console.log("Payment from:", session.customer_email); // TODO: Insert in DB, send email } return Response.json({ received: true }); } ``` ### Test Met `stripe listen` aan + dev server aan, doe een test-betaling. Check console output van beide processen. ### Eisen - [ ] Webhook endpoint werkt - [ ] Signature verification actief (test door secret te wijzigen — moet falen) - [ ] `checkout.session.completed` event wordt herkend - [ ] Console toont event-data --- ## Onderdeel B — Resend welkomstmail (verplicht) ### Setup ```bash pnpm add resend ``` Maak account op https://resend.com, kopieer API key. `.env.local`: ``` RESEND_API_KEY=re_... ``` ### Email versturen in webhook ```typescript import { Resend } from "resend"; const resend = new Resend(process.env.RESEND_API_KEY!); // In webhook handler, na event.type check: if (event.type === "checkout.session.completed") { const session = event.data.object; if (session.customer_email) { await resend.emails.send({ from: "Premium ", to: session.customer_email, subject: "Welkom bij Premium!", html: `

Welkom!

Bedankt voor je aanmelding.

`, }); console.log("Email sent to:", session.customer_email); } } ``` ### Optioneel: React Email Voor mooiere templates: ```bash pnpm add react-email @react-email/components ``` `emails/WelcomeEmail.tsx`: ```tsx import { Html, Heading, Text, Button } from "@react-email/components"; export function WelcomeEmail({ email }: { email: string }) { return ( Welkom! Je account ({email}) is geactiveerd. ); } ``` In webhook: ```typescript await resend.emails.send({ from: "...", to: session.customer_email!, subject: "Welkom!", react: WelcomeEmail({ email: session.customer_email! }), }); ``` ### Eisen - [ ] Email wordt verstuurd bij succesvolle betaling - [ ] Email komt aan in inbox (check je eigen email als test) - [ ] Resend dashboard toont email in logs --- ## Onderdeel C — Retry-logic (verplicht) Maak een retry-helper en gebruik 'm voor minimaal één externe call. `lib/retry.ts`: ```typescript export async function retry( fn: () => Promise, attempts = 3, baseDelayMs = 1000 ): Promise { for (let i = 0; i < attempts; i++) { try { return await fn(); } catch (err) { if (i === attempts - 1) throw err; const delay = baseDelayMs * Math.pow(2, i); console.log(`Retry ${i + 1}/${attempts} na ${delay}ms`); await new Promise(r => setTimeout(r, delay)); } } throw new Error("unreachable"); } ``` Gebruik: ```typescript const session = await retry(() => stripe.checkout.sessions.create({...}) ); ``` ### Demonstratie van een falende call Voor je `APIS.md` (zie D): induceer expres een fail om retry te laten zien. Bv. wrong URL of fake timeout. ### Eisen - [ ] `retry()` helper bestaat en werkt - [ ] Wordt gebruikt op minimaal één externe call - [ ] Log laat zien wanneer retries gebeuren - [ ] In APIS.md: 1 voorbeeld van failed call + retry succes --- ## Onderdeel D — `APIS.md` (verplicht) In repo-root. ### Sectie 1 — Welke APIs gebruikt jouw app Tabel: | API | Waarvoor | Auth | Cost | |-----|----------|------|------| | Stripe | Betalingen | secret key + webhook | 1.4% + €0.25 | | Resend | Transactional email | api key | gratis tier 3k/maand | ### Sectie 2 — Webhook setup Stappen die je nam: - Stripe CLI installeren - `stripe listen` command - Endpoint code (link naar file) - Signature verify (screenshot van falende verify als bewijs) ### Sectie 3 — Voorbeeld successful flow End-to-end: 1. Pricing page klik 2. Stripe Checkout (screenshot) 3. Test-betaling (screenshot) 4. Console output van webhook 5. Email screenshot 6. Stripe dashboard transactie (screenshot) ### Sectie 4 — Failed call met retry Voorbeeld waar retry redde: ``` Attempt 1: Failed (network timeout) Attempt 2: Failed (network timeout) Attempt 3: Success — session created Total time: 4.2s ``` Welke fout je induceerde + waarom retry hielp. ### Sectie 5 — Eindopdracht-toepassing Concrete plannen voor je eigen eindopdracht: - Welke externe API ga je gebruiken? - Waarom die? - Webhook nodig? Welk event? - Email-flows die je wilt? - Wat ga je doen aan rate limiting? Minstens 200 woorden, concreet. ### Vorm - Max 800 woorden totaal - Screenshots zijn waardevol - Concrete code-references (links naar files) --- ## Bonus (optioneel) ### Bonus 1 — OAuth login met Auth.js Voeg GitHub login toe met Auth.js. Documenteer setup-stappen. ### Bonus 2 — Idempotency in webhook Sla event.id op in Supabase, skip als al verwerkt. Documenteer waarom belangrijk. ### Bonus 3 — Rate limiting Voeg Upstash Ratelimit toe aan je `/api/checkout` endpoint. 5 calls per minuut per IP. ### Bonus 4 — Refund flow Webhook ook luisteren naar `charge.refunded` event. Email sturen + DB update. --- ## Inleveren 1. **GitHub repo URL** in Brightspace 2. **`APIS.md`** in repo-root (5 secties) 3. **Working Stripe + Resend** demonstreerbaar 4. **Screenshots** in APIS.md van flow + Stripe + Resend dashboards --- ## Beoordeling | Criterium | Punten | |-----------|--------| | A — Webhook werkt + signature verify | 3 | | B — Resend email bij betaling | 2 | | C — Retry helper + bewijs werking | 2 | | D — APIS.md compleet (5 secties) | 3 | | **Totaal** | **10** | Voldoende = 6+. Bonus telt mee. --- ## Tijd-indicatie | Onderdeel | Tijd | |-----------|------| | A — Webhook + verify | 30 min | | B — Resend email | 25 min | | C — Retry logic | 20 min | | D — APIS.md | 45 min | | **Totaal** | **~2 uur** | --- ## Veelvoorkomende valkuilen | Probleem | Oplossing | |----------|-----------| | Signature verify faalt | Gebruik `req.text()` niet `req.json()` | | Email komt niet aan | Check spam, check Resend logs | | `stripe listen` toont 401 | `stripe login` opnieuw | | Webhook secret klopt niet | Stripe CLI geeft een nieuwe secret per `stripe listen` sessie | | Resend "domain not verified" | Gebruik `onboarding@resend.dev` voor demo | | Headers reading errors | `await headers()` in Next 15+ | --- ## Tips - **Test Stripe alleen in test mode** — productie key kan echte geld kosten - **Stripe CLI is essentieel** voor lokaal testen - **Test cards lijst:** https://docs.stripe.com/testing - **Eindopdracht-tip:** kies één paid API (Stripe, Resend, Twilio) — laat zien dat je productie-flows snapt - **Webhook idempotency** is een real-world musthave — bonus laat zien dat je productie-mindset hebt