Work · Products · 13
Knock
Local services booking, built end to end.
Private repository. The live application above is the real thing.
- Type
- Products
- When
- 2026 · shipped
- Role
- Sole designer and developer. Product shape, data model, both interfaces, deployment.
- Scale
- Two-sided · one calendar seen from two directions
In plain language
Knock is a booking site for local services: a plumber, a cleaner, a tutor. A customer finds a provider, sees which times are genuinely free, and takes one. The provider sees the same calendar from the other side and manages what arrives. Almost none of the work is in the pages you can see. It is in making sure two people can never both walk away believing they hold the same eleven o'clock on Thursday.
The question
How do you let two strangers book against one calendar at the same instant without telling either of them something untrue?
Rough sketch
Where things sit on the screen, and in what reading order. Drawn, not screenshotted, so it stays true when the styling moves on.
Provider detail
Provider queue
What it does
Browse local service providers, see real availability, book a slot, manage the appointment afterwards. Two sides, customers and providers, with different views of the same calendar.
The constraint that shaped the build
Two-sided booking is a scheduling problem wearing a CRUD costume. The moment two customers can request the same slot, the availability model has to be authoritative and the write path has to be race-safe. Reading availability is cheap; committing to it is not.
That pushed the design toward treating a booking as a claim on a slot rather than a row in a table. The slot is the thing with state, and a booking either wins it or does not.
Where the time went
Not the UI. The provider-side calendar, the timezone handling, and making a double-booking impossible rather than merely unlikely.
Why there is no code link
The repository is private. The application is live and the link above works. That is handled by the content schema rather than by memory: the project sets no repository, and the template renders no code link when there isn’t one.
How it is put together
Left to right, the path a request or a record takes through the system.
01
Browse
- Provider directory
- Service detail
- Availability calendar
Read-heavy, cacheable, and allowed to be slightly stale.
02
Claim
- Slot claim request
- Conflict check against held slots
- Accept or reject, never maybe
The single write path. Everything contends here on purpose.
03
State
- Slot records
- Booking records
- Provider profiles and hours
Slot is authoritative. The booking record is downstream of it.
04
Manage
- Customer appointments
- Provider queue
- Cancel and release
What happens, step by step
In plain language, in the order it happens.
Find someone
The customer browses providers by service, and opens one to see hours and availability.
See real availability
The calendar renders from the provider's own working hours minus everything already held, in the customer's timezone rather than the provider's.
Claim a slot
Choosing a time submits a claim, not a booking. The claim either wins the slot or is refused. There is no intermediate state a customer could misread as confirmed.
Both sides update
A won claim writes the booking and removes the slot from every other customer's view. The provider's queue picks it up from the same record.
Manage afterwards
Either side can cancel, which releases the slot back into availability rather than deleting history.
Decisions, and what they cost
Every choice worth recording has a road not taken. Both are here.
Model the slot, not the booking
instead ofA bookings table with a uniqueness constraint bolted on
If the booking is the object, two simultaneous requests are both valid until a constraint rejects one, and by then the interface has already told both customers yes. Making the slot the thing with state means the contention is visible in the design instead of surfacing as a database error.
Reject a claim outright
instead ofQueueing or provisionally holding it
A pending booking is a promise the system cannot keep. Refusing immediately is worse UX for one second and better for everyone afterwards.
Store times against the provider's working hours, render in the customer's zone
instead ofStoring whatever the browser sent
Timezones are where booking apps quietly break. Keeping one canonical representation and converting only at the edge means a Berlin provider and a Hyderabad customer see the same appointment.
Two views over one calendar
instead ofSeparate customer and provider applications
Two applications means two versions of the truth, and reconciliation logic nobody asked for. One model, two projections.
What came out of it
- Live and usable end to end: browse, book, manage, cancel.
- Double-booking is structurally impossible rather than merely unlikely: it is the claim path, not a validation rule.
- Timezone handling is correct across the customer/provider boundary, which is the failure most booking demos have.
Still open
- Provider-side recurring availability, which is currently entered per week.
- Notifications. Right now the queue tells the provider; nothing pushes to them.
- The public repository stays private, so the live application is the only artefact.
Built with
- TypeScript
- Next.js
- Vercel