A Tipzo-style tip/donation app: a donor-facing tipping page and a transparent OBS overlay that pops up a live alert the instant a tip is paid.
- Tipping page:
/tip/[username] - OBS overlay:
/alert-box?username=[username]
- Next.js 14 (App Router, Route Handlers)
- React 18 + Tailwind CSS
- Razorpay Checkout +
razorpayserver SDK - Pusher Channels (
pusherserver SDK +pusher-jsclient) for real-time delivery to the overlay - Framer Motion for the alert pop-in/out animation
- lucide-react for icons
npx create-next-app@latest tipzo --typescript --tailwind --app --no-src-dir --import-alias "@/*"
cd tipzo
npm install razorpay framer-motion lucide-react pusher pusher-jsIf you're starting from the files in this project directly, just run:
npm install
Copy the example file and fill in your real keys:
cp .env.local.example .env.local# Razorpay — https://dashboard.razorpay.com/app/keys
RAZORPAY_KEY_ID=rzp_test_xxxxxxxxxxxxxx
RAZORPAY_KEY_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
# Pusher — https://dashboard.pusher.com (Channels app)
PUSHER_APP_ID=1234567
PUSHER_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_PUSHER_KEY=xxxxxxxxxxxxxxxxxxxxxxxx
NEXT_PUBLIC_PUSHER_CLUSTER=ap2Notes:
RAZORPAY_KEY_SECRETandPUSHER_SECRET/PUSHER_APP_IDare server-only and are never sent to the browser.NEXT_PUBLIC_PUSHER_KEYandNEXT_PUBLIC_PUSHER_CLUSTERare intentionally public — the Pusher app key is meant to be visible client-side so the browser can open a socket. Access control happens via HMAC-signed private channels if you later need to lock things down; this project uses public per-creator channels (alerts-<username>) since alert data isn't sensitive.
Drop an MP3 at public/alert-sound.mp3 — the overlay plays it automatically
when a tip comes in. Keep it short (1–3s) so it doesn't overlap the next tip.
npm run dev- Tip page:
http://localhost:3000/tip/demo-creator - Overlay:
http://localhost:3000/alert-box?username=demo-creator
Open both in separate tabs, send a test tip (use Razorpay test mode + test card numbers), and watch the alert pop up on the overlay tab.
- In OBS, add a Browser Source.
- URL:
https://yourdomain.com/alert-box?username=your-username - Width/height:
500 x 300(adjust to taste — the popup is centered and top-aligned). - Check "Shutdown source when not visible" OFF, and "Refresh browser when scene becomes active" OFF, so it keeps listening between alerts.
- The page background is transparent, so no green-screen chroma key needed.
donor fills form on /tip/[username]
│
▼
POST /api/create-order ──▶ Razorpay: orders.create() ──▶ { order_id }
│
▼
Razorpay Checkout modal opens client-side (checkout.js)
│
▼ (on successful payment)
POST /api/verify-payment
│ 1. recompute HMAC SHA256(order_id|payment_id) with key secret
│ 2. crypto.timingSafeEqual() against razorpay_signature
│ 3. if valid → pusherServer.trigger("alerts-<username>", "new-tip", payload)
▼
Pusher Channels broadcasts the event
│
▼
/alert-box (subscribed to "alerts-<username>") receives "new-tip"
│
▼
Framer Motion pop-in → plays audio → auto-hides after 6s
| File | Purpose |
|---|---|
app/tip/[username]/page.tsx |
Tipping page shell (server component) |
components/tip/TipForm.tsx |
Client form: amount, message, Razorpay checkout flow |
components/tip/AmountSelector.tsx |
+/- stepper + quick-select pills |
components/tip/ProfileHeader.tsx |
Avatar, handle, platform badge |
app/alert-box/page.tsx |
OBS overlay: subscribes to Pusher, queues + shows alerts |
components/overlay/AlertPopup.tsx |
Framer Motion alert card |
app/api/create-order/route.ts |
Creates a Razorpay order (amount validated server-side) |
app/api/verify-payment/route.ts |
Verifies HMAC signature, broadcasts via Pusher |
lib/razorpay.ts |
Server Razorpay SDK instance |
lib/pusher-server.ts |
Server Pusher SDK instance |
lib/pusher-client.ts |
Client Pusher singleton |
lib/pusher-shared.ts |
Channel-name/event-name constants shared by both |
lib/types.ts |
Shared TypeScript types for order/verify/alert payloads |
- Persist tips to a database. The current
verify-paymentroute broadcasts immediately after signature verification but doesn't persist anything — add a database write (Postgres/Supabase/Mongo) before thepusherServer.trigger(...)call so you have a durable record and can rebuild history, show a leaderboard, resend receipts, etc. - Webhook as a safety net. The flow above verifies payment via the
client-returned signature, which covers the common case. For full
reliability (e.g. the donor closes the tab right after paying), also
register a Razorpay webhook (
payment.captured) pointing at a route that performs the same HMAC check usingX-Razorpay-Signatureand your webhook secret (different from the key secret), then triggers the same Pusher event — so the alert fires even if the client never calls/api/verify-payment. - Rate limiting. Add rate limiting to
/api/create-orderto prevent abuse (e.g. via Upstash Ratelimit or a simple IP-based check). - Currency/amount limits. Adjust
MIN_AMOUNT/MAX_AMOUNTinapp/api/create-order/route.tsto your needs.