Case study · Founder & CTO
Zen-Z
Small groups of strangers, one plan, and nobody has to be the organiser.
- Role
- Founder & CTO, sole engineer
- Timeline
- Jul 2026 – now
- Platforms
- Android, iOS (PWA) and web
- Stack
- React Native, Next.js, Supabase, PayU



- Problem
- Meeting new people at college depends on luck, or on someone organising.
- Built
- The student app, the admin dashboard and the Supabase backend with PayU payments. Solo.
- Result
- Live on Google Play: 150+ users in its first 2 days, ₹33K+ revenue.
01 — Problem
Meeting people shouldn't depend on luck.
Or on someone volunteering to organise.
Zen-Z matches college students into small groups of strangers (4–5 people; sports up to 14) and gives each group a plan: a café, a dinner, a movie or a game. Cafés and dinners are free to book (venues pay a commission); movies and sports are paid. Demand came first: 65+ students joined the waitlist before launch.
02 — What I built
App, admin, backend. One engineer.
Product decisions, data model, app, internal tools, payments, CI and releases.

01
Pick an activity
Box cricket, football, pool or pickleball.

02
Pick a slot
One weekly time, so groups form.

03
Set a budget
So groups spend alike.

04
Choose the group
Mixed, women only or men only.

05
Confirm the plan
Review every choice before paying.

Pay
Pay to hold the seat
PayU checkout; only a verified webhook confirms it.
The student app
Android, iOS (PWA) and web from one codebase, 20 screens, realtime group chat, push notifications.
The admin dashboard
A drag-and-drop board the team uses to hand-match every group, plus moderation and analytics.
The backend
Postgres with RLS on all 28 tables, 7 Edge Functions, 3 cron jobs, PayU payments.
03 — The website
zen-z.site: a Z you can pull.
I redesigned the marketing site from scratch: cream paper, Zen-Z red, stickers and doodles.



- 1
A Z you can pull
The red Z hangs on a cord you can drag, with rope physics I wrote by hand (~150 lines, no 3D library). The server renders a still Z first, so nothing needs JavaScript.
- 2
Payments never broke
27 contract checks guard the PayU payment-return URL, the iPhone install flow and every legal page. They pass against production.
- 3
98 on Lighthouse
Desktop performance on the live site (median of 3 runs), and 100 for accessibility, best practices and SEO.
04 — Architecture
Boring, proven parts. The database enforces the rules.
Access rules live in the database (row-level security on all 28 tables), so they hold even if the app has a bug. Money only moves in Edge Functions, and a trigger seals free bookings so the client can’t fake a ₹0 price.
App
React Native + Expo
20 screens · Android, iOS (PWA) and web from one codebase
Admin dashboard
Next.js
Drag-and-drop board to hand-match every group, moderation, analytics
Backend
Supabase
- Postgres28 tables, RLS on every one
- Edge Functions7, Deno
- Realtimegroup chat
- Authemail OTP
- pg_cron3 jobs
Payments
PayU
Orders created on the server. SHA-512 signed webhooks, verified in constant time, confirm the seat.
05 — Incident
A paid booking never confirmed. I fixed it the same day.
16 Sep 2026Fixed the same day
What shipped that day
- Every webhook call is logged with its outcome in
payment_webhook_events. - An admin “Needs your attention” queue: unpaid for 10+ minutes, or paid but the slot has passed.
- An admin-only
admin_mark_booking_paid()RPC, covered by a pgTAP test, a WhatsApp support link on the payment screen and a “Payments to check” count on the dashboard.
The guarantee now
No booking is confirmed without a verified payment, every webhook call is on record, and anything stuck reaches the admin queue.
What happened
Paid
A student's real UPI payment succeeds.
No webhook
PayU never calls back, and webhook calls aren't logged yet.
Traced
Same day: confirmed the payment in PayU; no webhook had been logged.
Fixed · 14 files
Shipped the fix the same afternoon.
The verification was already in place: PayU’s docs give the wrong webhook hash for Payment Links, so on 29 Aug I worked out the real one from a live ₹1 transaction, and only a signed, server-verified webhook could confirm a booking. On 16 Sep, a student’s UPI payment went through but PayU’s webhook never landed, and webhook calls weren’t logged yet, so there was no trail and the booking still showed unpaid.
A week later, checkout moved to its own tab and the booking re-checks itself when the student returns (Payment Links never redirect back). Cancel-then-pay on a stale tab is logged for a manual refund, not confirmed.
Deno.serve(async (req) => {
const p = Object.fromEntries(await req.formData());
// PayU's docs show a 5-udf formula. Payment Links really use all ten,
// confirmed byte for byte against a live ₹1 transaction.
const expected = await sha512([
SALT, p.status,
p.udf10, p.udf9, p.udf8, p.udf7, p.udf6,
p.udf5, p.udf4, p.udf3, p.udf2, p.udf1,
p.email, p.firstname, p.productinfo, p.amount, p.txnid, KEY,
].join("|"));
// Constant-time compare: a bad signature confirms nothing.
if (!timingSafeEqual(expected, p.hash)) return reject(p, "invalid_signature");
// Cancelled, then paid on a stale tab? Logged, not confirmed.
const outcome = await settleBooking(p);
await logEvent(p, outcome);
return ok();
});
// Best-effort: logging never changes the response.
async function logEvent(p, outcome) {
try {
await db.from("payment_webhook_events").insert({ txnid: p.txnid, outcome });
} catch {}
}06 — Results
Paying students, live on Play.
Live on Google Play, and pitched to the judging panel at JNU's Shark Tank (Sep 2026).
- users in the first 2 days after launch
- 150+
- total revenue so far
- ₹33K+
- students on the waitlist before launch
- 65+
- views on Instagram · 550+ followers
- 140K+
Under the hood
- Tables, row-level security on every one
- 28
- SQL migrations
- 102
- Postgres functions
- 41
- Edge Functions
- 7
- pg_cron jobs
- 3
- CI jobs
- 11
- Test suites in CI
- 37
- pgTAP assertions
- 115
- Commits
- 190
Along the way
- Moved payments from Razorpay to PayU mid-build.
- Got past a Play Store rejection with a reviewer sign-in path.
- Writing the pgTAP tests found and fixed 3 RLS and authorization bugs.
