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

Customer-side booking and the provider's queue, at the level of what sits where.

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.

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.

  1. 01

    Browse

    • Provider directory
    • Service detail
    • Availability calendar

    Read-heavy, cacheable, and allowed to be slightly stale.

  2. 02

    Claim

    • Slot claim request
    • Conflict check against held slots
    • Accept or reject, never maybe

    The single write path. Everything contends here on purpose.

  3. 03

    State

    • Slot records
    • Booking records
    • Provider profiles and hours

    Slot is authoritative. The booking record is downstream of it.

  4. 04

    Manage

    • Customer appointments
    • Provider queue
    • Cancel and release
The slot is the object with state; a booking is what happens when a claim on a slot succeeds. Every write funnels through the claim path, which is the only code in the system allowed to say yes.

What happens, step by step

In plain language, in the order it happens.

  1. Find someone

    The customer browses providers by service, and opens one to see hours and availability.

  2. 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.

  3. 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.

  4. 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.

  5. 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