©Ridge NelsonWebsites & systems

Facility OS

← All work

04Product
Operations
Software

Booking, payment and timed door access for a facility with nobody on site, and one screen the owner runs the day from.

Role
Product design and build, with the owner.
Status
Client work, built 2026. Covered by 43 tests.
Stack
Next.js, TypeScript, PostgreSQL, Stripe (mock providers), Tailwind
The owner’s dashboard for one day: two bookings, $99.00 confirmed revenue and four active bays; each bay’s sessions with customer and door code; a form to block a slot for maintenance; and the outbox of every text and email the system sent.
Owner

The whole day on one screen. Demo data.

Owner

With no staff on site, the software is the staff.

Most booking software stops at the confirmation email, which leaves the owner checking a calendar, a payment processor and a lock in three places. Here it is one screen.

  1. The day at a glance: bookings, revenue, bays, and every door code.
  2. Cancel a booking, or block a slot for maintenance or a private event.
  3. Retry: a paid booking whose code never issued is flagged, with one button to issue it again.

Customer

A booking and a person at the door are the same event.

The customer picks a bay, a date and a length and only sees start times that are free. A card hold is taken at booking, and a six-digit code arrives on screen, by text and by email. It works inside the session window and nowhere else.

Booking a bay: bay, date and a two-hour duration chosen; a grid of start times that are genuinely free, with 6:00 to 8:00 PM selected; and a reservation summary with a $10 card hold and a Reserve button.
Only free start times are offered.
The confirmation on a phone: “You’re booked on Bay 3”, 7:00 to 8:30 PM, a first-session price, and a six-digit door code that works only during the session window.
The code is valid for that session only.

Rules

Three questions the system cannot get wrong.

Is this bay free? Does this person hold a valid booking? Should the door open now? A wrong answer on a screen is a support ticket. At the door it is a customer locked out, or a building open to the wrong person.

  • Codes scoped to the sessionEach confirmed booking gets a code valid from its start to its end, issued through a lock-provider interface.
  • Owner access checked twiceA signed, httpOnly session. The edge turns away unauthenticated owner pages, and every owner action checks again.
  • Idempotent paymentsA repeated authorisation with the same key replays the original instead of charging a second time.
  • 43tests
  • 10test files
  • 3swappable providers

Recovery

Payment can succeed while the lock fails.

This was the hard part. The obvious design takes the money and issues the code in one flow, until the lock or the message provider is down for ninety seconds and somebody has paid for a session they cannot get into.

My first answer was an automatic refund, which was wrong: it hands back a slot the customer still wants. So the failure gets a state of its own. The booking becomes paid_needs_code, the slot stays held, and it shows at the top of the owner’s dashboard with a Retry button.

  1. 01 Reserved Slot held, card authorised.
  2. 02 Paid Captured once, idempotent on retry.
  3. 03 · the failure paid_needs_code The lock or messaging failed. Slot held, no refund.
  4. 04 Confirmed The owner retries and the code issues.
The booking path

Four states, and the third is the one that pays for the other three. A test covers it.

Two decisions

  1. Two people, one slot

    Choice
    An atomic check in the app, backed by a Postgres exclusion constraint per bay.
    Reason
    The app check gives the customer a clean answer. The constraint holds even if a future code path forgets to ask.
  2. The server’s clock is not the venue’s

    Choice
    Every day boundary, slot and code window is computed in the venue’s time zone, and the test suite forces UTC.
    Reason
    A code that opens an hour late is a customer locked out, and the bug would never appear on a developer’s machine.

Current state

Built for the client and covered by 43 tests. Payments, the lock and messaging run on mock providers today, each behind one adapter, so going live is one swap per provider. The captures here are demo data.

Open: the owner’s dashboard is built for a laptop and does not fit a phone screen yet.

The client’s name, location and access vendor are withheld at their request.

Next · 05 Acquisition engine