Refill engineering guideMBA guide →

Fynd × NMIMS Hyderabad · case C29 · 17–18 Oct 2026

Refill

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.

What we built, in one minute

  1. A customer cancels. The owner taps the appointment: Customer cancelled. Under 2 hours notice is marked late.
  2. The gap glows. The panel lists who fits it, and who is skipped with a rule code.
  3. The owner reviews and sends offers. The database re-checks every rule first.
  4. The customer gets a phone page: Book, Not today, or Stop messages.
  5. Two taps at once: one booking. The other customer keeps their queue place.
  6. Every step lands in the Evidence log, exportable as JSON or CSV.

How we are judged, and where each point comes from

Rubric rowPtsWhat we show
Working user journey25The 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 slot10Rule 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 slot10Row 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 outreach5Consent 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 handling15Bad 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 feedback15Tag 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 reproducibility10npm install, npm test, npm run race; no keys needed; README; CI runs tests on every pull request; Vedanth's clean-machine check.
Individual technical contribution10One question each. Everyone's work is in their own files, branch and pull request, recorded in docs/contributions/.

How it works

Rules engine

src/engine.ts

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

Supabase Postgres

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

accept_offer

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

React + Vite + TypeScript

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.

Tech stack, and why

PieceChoiceWhy
UIReact 19, Vite, TypeScriptFast to build, typed end to end, tiny production bundle (85 kB gzipped).
DataSupabase Postgres, SQL functionsTransactions and row locks are the honest answer to double booking. Free tier, Mumbai region.
HostingVercelPush to main deploys. Security headers and a content security policy in vercel.json.
TestsVitest + a live race script30 unit and scenario tests in under a second; the race script hits the real database.
InstallWeb app manifest + service workerAdd to home screen on Android, iPhone and desktop. The shell opens offline; bookings stay live.

The 4-minute demo (draft, Vedanth owns the final)

TimeOn screenSay
0:00Entry screen"An empty chair is money walking out of the salon. Refill fills it, with consent, without double booking."
0:20Desk, tap Simran 12:30 → Customer cancelled"A customer cancels 75 minutes before. Refill marks it late and opens the gap."
0:45Recovery panel"Who fits, and who is skipped and why. Arnav's consent is unknown, so he is never messaged."
1:15Send offers, show QR codes"The database re-checks every rule before an offer exists."
1:40Two phones tap Book together"Two taps, same millisecond: one booking. The other keeps their place."
2:20Phone: Stop messages"Consent withdrawn: every open offer is revoked instantly."
2:45Evidence log, export"Every decision is explained and exportable."
3:05Lab: checks, live race"All three case checks run live; here is a real race at the real database."
3:35What changed after MBA feedback"Our business team asked for staff-aware matching; here is before and after."

Questions judges will ask

How do you guarantee two people never get the same slot?

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.

Why not check availability in the app before booking?

Two phones can both see "free" at the same moment. Only the database can serialise them. The page previews; the database decides.

What if a customer taps Accept twice?

The second tap returns ALREADY_CONFIRMED with the same booking number. Idempotent: one booking.

What does consent mean in your system?

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.

Can someone bypass the page and write to your tables?

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.

Is it protected against abuse or a DDoS?

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.

What if nobody replies to an offer?

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.

What happens with a bad input file?

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.

What if two judges use the app at once?

Each visitor gets a separate workspace (a uuid in the link). Share the link to open the same day on a phone.

How do you know your rules are right?

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.

What does it not do yet?

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.

Your part, and your question

Dhruv · 3rd year

Product and full-stack build

Engine, database functions, all screens, deployment. Your question: "Walk us through the lock order in accept_offer."

Vedanth · 4th year

Reliability and handoff

Clean-machine setup check, race test runs, known limits, the demo script. Your question: "Could someone else run and trust this?"

Parth · 2nd year

Test scenarios

tests/scenarios.json, run on every push. Your question: "How do you know it is correct?"

Chirag · 2nd year

Salon data and shocks

data/salon/*.csv and the four shock scenarios. Your question: "Where does your data come from and what does it prove?"

9 AM market shock: what we show

ShockIn the app
Late cancellationMove 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 absenceLab 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 durationLab toggle "Longer service": the match disappears with DURATION_TOO_LONG. A shorter service may still take the slot.
Consent withdrawnPhone: Stop messages, or the desk's consent toggle. Open offers revoked; a booking attempt is refused.

Commands

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