Skip to main content
This document captures the mechanics: the booking state machine, ID/number formats, GST and gems math, all 19 background jobs, request lifecycles, indexes, and security implementation. Money is always integer paise; status transitions are always logged in booking_status_log.

Booking State Machine

Walk-ins skip pending — they go directly to confirmed. Every status transition is recorded in booking_status_log with timestamp, actor (user ID), and reason.

ID & Number Formats

  • H = salon (hair/beauty)
  • S = spa
  • -M suffix = membership session

GST Calculation

All prices are GST-inclusive (18%, SAC 999721). Back-calculate the taxable base:
CGST and SGST are equal halves (intra-state, Karnataka). The base is rounded; GST is the remainder — so base + gst always reconstructs the original inclusive amount exactly.

Gems (Loyalty) Calculation

No-Show Policy Logic

Recovery: 3 consecutive completed bookings reset no_show_count. Walk-in no-shows do NOT count.

Background Jobs (19 Total)

QStash scheduled jobs — including the seven that were formerly pg_cron.

Realtime Channels (Ably)

Token Auth: clients receive a scoped Ably JWT from POST /api/ably/token. The token is scoped to only the channels the user is allowed to subscribe to (based on their role and user ID).

Request Lifecycles

The four core write paths, step by step.
Customer submits the booking form → POST /api/bookings.
1
Validate session (requireSession).
2
Zod-validate the request body.
3
Check slot conflicts against Neon; no Redis availability cache is implemented.
4
Validate all service IDs exist and are active.
5
Get the default staff for each service.
6
Generate the booking number (BK-RS-YYMM-H/S-XXXXX).
7
INSERT the booking row (status: pending).
8
INSERT booking_service rows (price snapshot).
9
Enqueue QStash job 17 (stale booking alert, +2h).
10
Publish an Ably event to admin:bookings:{branchId}.
11
Return { success: true, data: { bookingId, bookingNumber } }.

Database Indexes

Key indexes for query performance:

Security Implementation

Every API route validates input with Zod .safeParse() before any business logic runs. Raw client input never reaches the database.
@upstash/ratelimit with a sliding-window algorithm, applied in Next.js middleware before auth checks.
Strict Content-Security-Policy with nonce-based script loading, configured in next.config.ts.
Drizzle ORM uses parameterized queries exclusively. No raw SQL string concatenation anywhere in the codebase.
Exact origin matching (theroyalglow.in only). No wildcard.
All inbound webhooks (QStash, Meta, AiSensy) verify HMAC signatures before processing.

High-Level Design

Architecture, decisions matrix, and NFRs

Data Model

All 38 tables, enums, and conventions

Background Jobs

Full job inventory and heartbeats
Last modified on September 1, 2026