← Back to the case study

Engineering notes

Almost every rule in BookHere got its exact shape from something going wrong. So the list of faults is not an apology, it is the argument for every rule that followed.

The faults, and what each one broke

Two bookings could take the same slot. A function that checks then inserts cannot hold the rule under a real race. → The rule moved into the database as a GiST exclusion constraint, so the second write is refused by Postgres itself, not by hope.

Deleting a service blanked its name on every past booking. The foreign key was ON DELETE SET NULL, and it had already happened four times. → The name is copied onto the booking with a trigger and read through a coalesce. What was agreed stays agreed.

The rate limits counted nothing. Counters lived in per-instance Worker memory, so thirty wrong codes returned thirty tries and never a wait. → Every counter moved into a database table, keyed per address, then re-measured until the eleventh call was actually refused.

A refusal counted itself. Each limiter recorded the attempt before deciding to allow it, so a blocked user reloading to check extended their own ban. → Decide first, record second.

The reminder clock was losing its own evidence. 52 of 78 sweeps timed out on pg_net's five-second default; the letters had gone, but a real failure would have looked identical. → Timeout raised to 30 seconds, still inside the five-minute tick so sweeps cannot overlap.

The error surface leaked the schema. The reschedule route answered the internet with function names and argument names from the schema cache. → Only P0001 errors, written on purpose, reach the public as a 400; everything else becomes "That did not go through."

Nineteen functions were open to anyone and called by nothing. Two of them would have let a stranger overwrite a shop's hours and services. → Migration 090 revoked all nineteen. The front door is now short enough to read in one sitting.

Every QR code looked perfect and scanned on nothing. Format bits written least-significant-first, the second format copy one bit late, and alignment squares crossing the timing line. → Found only by decoding all 534 with an independent reader, never by eye.

What a fault costs at each stage

Found by reading the code: free.

Found by testing on a live page: almost free.

Found by a customer: expensive, because the shop hears about it, and the shop is the one who looks incompetent to their own customer.

Found by a customer who then did not turn up: the most expensive, because nobody ever tells you it happened.

How to find the next one

Open, and listed as open

Naming your own unfixed problems in public is a stronger signal than any feature. These are real and still open.

The session token lives in localStorage. The httpOnly-cookie fix is real work, not a night's work, because it changes how every request is authenticated. It is a known trade, not a thing I have not noticed.

The sign-in door admits when an address has no page. That is a genuine trade between a helpful message and leaking whether an account exists, and it is on the list as a trade, not a bug.

Deposits are built and switched off. The code exists; taking money before the shop has proven the flow is a decision I have chosen to defer.

“Inherited is not chosen.”

“I should have looked before saying it.”

Two lines I keep from building this. The first is why I re-check anything I did not decide myself. The second is why I decode the QR code instead of looking at it.

← The case study Portfolio