Fynd × NMIMS Hyderabad · case C29 · 17–18 Oct 2026
An empty chair is money walking out of the salon. Refill offers the cancelled slot to the next waiting customer who fits it, checks consent before every message, and never books two people into one chair.
| Rubric row | Pts | What we show |
|---|---|---|
| Working user journey | 25 | The six steps above, live, from a fresh demo day. Every visitor gets their own workspace, so judges never collide. |
| Case check 1: 60-minute request vs 30-minute slot | 10 | Rule DURATION_TOO_LONG in the engine and in the database (fit_reason, checked at outreach and at booking). Lab check 1, tests C01–C02. |
| Case check 2: two requests, one slot | 10 | Row lock in accept_offer plus a one-booking-per-slot index. Desk button "Two phones, one slot", Lab "Run live race", npm run race. |
| Case check 3: no consent, no outreach | 5 | Consent is the first rule; unknown counts as no; checked when the offer is sent and again right before booking; STOP revokes open offers. |
| Reliability and failure handling | 15 | Bad input stops with DUPLICATE_ID / MISSING_FIELD / INVALID_VALUE; repeated taps are idempotent; offline banner; slots too close to start are never offered. |
| Improvement after business feedback | 15 | Tag v1-mba-handover is what the MBA team inherited. Their request lives in an mba-request issue; the shipped change gets its own tag. |
| Handoff and reproducibility | 10 | npm install, npm test, npm run race; no keys needed; README; CI runs tests on every pull request; Vedanth's clean-machine check. |
| Individual technical contribution | 10 | One question each. Everyone's work is in their own files, branch and pull request, recorded in docs/contributions/. |
Rules engine
Pure functions. For each free slot it checks every waitlist entry in this order and stops at the first failure: consent → skill → duration → availability. Free slots are filled in order; each takes the first eligible person in the queue; nobody gets two slots.
Database
Every table has row level security on and no policies, so the public key cannot read or write tables. The browser can only call 12 functions, and each one re-checks the rules. The page's suggestions are never trusted.
No double booking
Locks the offer, then the slot, then the customer, in that order everywhere. Simultaneous taps queue on the slot lock: the first finds it free and books; the rest find it booked and keep their place. A partial unique index is the last line of defence.
Front end
Three screens: Desk (owner), Offer (customer's phone), Lab (judges). Talks to the database with plain fetch (15 lines, no client library). Polls every 1.2 s. Installable as an app; works on phones.
| Piece | Choice | Why |
|---|---|---|
| UI | React 19, Vite, TypeScript | Fast to build, typed end to end, tiny production bundle (85 kB gzipped). |
| Data | Supabase Postgres, SQL functions | Transactions and row locks are the honest answer to double booking. Free tier, Mumbai region. |
| Hosting | Vercel | Push to main deploys. Security headers and a content security policy in vercel.json. |
| Tests | Vitest + a live race script | 30 unit and scenario tests in under a second; the race script hits the real database. |
| Install | Web app manifest + service worker | Add to home screen on Android, iPhone and desktop. The shell opens offline; bookings stay live. |
| Time | On screen | Say |
|---|---|---|
| 0:00 | Entry screen | "An empty chair is money walking out of the salon. Refill fills it, with consent, without double booking." |
| 0:20 | Desk, tap Simran 12:30 → Customer cancelled | "A customer cancels 75 minutes before. Refill marks it late and opens the gap." |
| 0:45 | Recovery panel | "Who fits, and who is skipped and why. Arnav's consent is unknown, so he is never messaged." |
| 1:15 | Send offers, show QR codes | "The database re-checks every rule before an offer exists." |
| 1:40 | Two phones tap Book together | "Two taps, same millisecond: one booking. The other keeps their place." |
| 2:20 | Phone: Stop messages | "Consent withdrawn: every open offer is revoked instantly." |
| 2:45 | Evidence log, export | "Every decision is explained and exportable." |
| 3:05 | Lab: checks, live race | "All three case checks run live; here is a real race at the real database." |
| 3:35 | What changed after MBA feedback | "Our business team asked for staff-aware matching; here is before and after." |
The booking happens inside one database transaction that locks the slot row. Concurrent taps wait in line. The first finds the slot free; everyone after finds it booked and is told which booking won. A unique index on confirmed bookings per slot backs it up. We test it with 8 and 12 simultaneous taps against the live database.
Two phones can both see "free" at the same moment. Only the database can serialise them. The page previews; the database decides.
The second tap returns ALREADY_CONFIRMED with the same booking number. Idempotent: one booking.
Yes, no or unknown, with a timestamp and a source. Unknown is never treated as yes. It is checked when the offer is sent and again right before the booking, so withdrawing consent between the two stops it. A STOP from the customer revokes every open offer.
No. Row level security is on with no policies. The public key can only call the 12 functions, and each one validates its inputs and re-checks the rules. Supabase's linter lists those functions as callable by anyone; that is intentional, they are the API.
The site is static on Vercel's edge network, which absorbs network-level floods. The database enforces its own limits per IP, per demo day and per offer (for example 240 writes a minute per day, 600 taps a minute per IP), caps payload and text sizes, revokes all direct table access from the public key and deletes demo days after 3 days. npm run dbtest proves each limit against the live database.
Every offer holds for 15 minutes (never past the slot's start). Then it lapses, the log says so, and the desk suggests the next people in the queue. Nobody is messaged more than 3 times a day.
It stops with a code (duplicate id, missing field, invalid value) and the exact location. A bad file never produces a half-right answer. Try it in the Lab.
Each visitor gets a separate workspace (a uuid in the link). Share the link to open the same day on a phone.
The organiser's fixture reproduces its expected match (W1 → S2). 18 core scenarios plus the team's own file run on every push in CI, and live in the Lab.
No owner login, no real WhatsApp/SMS sending, polling instead of a live socket, synthetic data only. All listed in the README and KNOWN_LIMITS.
Dhruv · 3rd year
Engine, database functions, all screens, deployment. Your question: "Walk us through the lock order in accept_offer."
Vedanth · 4th year
Clean-machine setup check, race test runs, known limits, the demo script. Your question: "Could someone else run and trust this?"
Parth · 2nd year
tests/scenarios.json, run on every push. Your question: "How do you know it is correct?"
Chirag · 2nd year
data/salon/*.csv and the four shock scenarios. Your question: "Where does your data come from and what does it prove?"
| Shock | In the app |
|---|---|
| Late cancellation | Move the demo clock, cancel an appointment inside 2 hours: tagged LATE, offers still go out; inside 10 minutes nothing is offered. Lab toggle "Late cancellation" with before/after export. |
| Staff absence | Lab toggle "Staff absence" blocks the slot. Staff-aware matching (who is in, who has the skill) is the change negotiated with the MBA team. |
| Different service duration | Lab toggle "Longer service": the match disappears with DURATION_TOO_LONG. A shorter service may still take the slot. |
| Consent withdrawn | Phone: Stop messages, or the desk's consent toggle. Open offers revoked; a booking attempt is refused. |
npm install npm run dev # local app on the shared demo database npm test # engine, fixture, scenarios, data npm run race # 8 simultaneous taps x 3 runs, live database npm run build
Synthetic demo. Kesar Salon and every person in it are invented. MBA guide · GitHub