GitHub
Manav — Astrology Booking Platform (Freelance) banner

Manav — Astrology Booking Platform (Freelance)

  • React
  • TypeScript
  • Bun
  • Express
  • PostgreSQL
  • Prisma
  • Redis
  • Razorpay

Overview

A full-stack, production booking platform for a professional astrologer. Customers browse consultation types, pick a time slot, pay, and receive a confirmed booking — including an auto-generated Google Meet link for video readings. The owner runs the entire business from a self-service admin dashboard: catalog, pricing, availability, calendar, revenue analytics, blog, and testimonials.

I was the sole full-stack engineer — design, build, deploy, and ops.

The hard problems here aren't the UI. They are never losing a paid booking and never double-selling the same time slot:

  • Money is involved. If a browser dies between "payment captured" and "booking saved," the customer has paid for nothing. That has to be engineered out, not hoped away.
  • Slots are a single, scarce resource. There is one astrologer, so two people must never pay for the same 3:00 PM slot. Under concurrency, naive check-then-insert logic double-books.

Architecture

Overview

Two independent deployables sharing a typed contract: a React 19 + Vite SPA and an Express 5 on Bun API, organized in routes → controllers → services layers with all business logic in individually unit-tested services.

Never lose a paid booking

  • The booking is not created optimistically in the browser. The moment a Razorpay order is created — before the user pays — the entire intake is persisted as a PendingBooking row with the raw request body as JSON.
  • After capture, the real booking can be finalized by either the browser callback or the server-side payment.captured webhook, whichever arrives first. Because the stored payload is verbatim, both paths run the exact same validated creation logic.
  • Idempotency is enforced in the database: a unique index on razorpayOrderId makes the second finalizer a harmless no-op instead of a duplicate booking.
  • The webhook is registered before express.json() and reads the raw body buffer, since Razorpay's signature is an HMAC over the exact bytes — parsing first would break verification.
  • If payment is captured but the booking can't be created, the pending row is marked FAILED with a reason so an admin can refund or reschedule, and a reconciliation script sweeps for orphaned checkouts.

Never double-sell a slot

  • Layer 1 — Redis hold: reaching the payment screen takes an atomic SET NX with a 10-minute TTL in Upstash Redis, so abandoned checkouts auto-expire with no cron. Release is a compare-and-delete Lua script, so a stale caller can never wipe a hold that has since been re-taken.
  • Layer 2 — DB constraint: a Postgres composite unique key on (bookingDate, timeSlot, bookingType) is the ultimate backstop; even if the cache layer is bypassed or degraded, the database physically rejects the second insert.
  • Fail-open by design: if Redis is unreachable the hold fails open rather than blocking all bookings, and logs loudly that the lock is degraded. Availability degrades; correctness does not.

Engineering trade-offs

  • Denormalized booking snapshots over foreign keys. Bookings store a copy of the service slug and name rather than an FK, so editing or deleting a service can never rewrite or break a historical record. Some duplication, in exchange for historical integrity.
  • Services as data, not code. No fixed enum of service types — per-service behavior (question count, slot window, whether it generates a Meet link) lives in the row, so the owner restructures the catalog without a redeploy.
  • OAuth2 refresh token over a service account. Consumer Gmail service accounts can't mint Meet links or invite attendees, so the owner connects their account once and the refresh token is persisted as a singleton.
  • IST everywhere. Financial metrics key off createdAt (when money moved) while schedule views key off bookingDate (when the consultation happens) — getting both date semantics right is what makes the analytics trustworthy.
  • Additive, transactional production migrations. Schema changes ship as additive, verified scripts — never destructive pushes against prod — with environment-isolated Neon databases for dev, test, and prod.