cd ..
cat space-tourism/README.md

Space Tourism

A seat-booking platform whose central constraint is that a launch must never sell past capacity — including under a rush of simultaneous buyers.

stack
React · TypeScript · Express · MongoDB · Redis · Stripe · Docker
livesource

Booking is the whole problem. Everything else — the catalog, the admin panel, the emails — is ordinary CRUD wrapped around one operation that has to be correct when several people click Book on the last seat at the same moment.

Reserving seats without a transaction

The seat count is decremented and guarded in one MongoDB operation, with the capacity check living in the query filter rather than in application code:

Launch.findOneAndUpdate(
  { _id, seatsAvailable: { $gte: seats }, status: "scheduled" },
  { $inc: { seatsAvailable: -seats } },
  { new: true },
);

There is no read-then-write window to lose. If the filter doesn’t match — because another request got there first and drove seatsAvailable below what this one needs — no document is returned, no booking is created, and the request gets a 409. Correctness comes from the database’s per-document atomicity, so it holds without a multi-document transaction or an application-level lock.

npm run loadtest fires 50 concurrent bookings at a launch with one seat. Exactly one wins; the other 49 get 409. It runs against a real Mongo instance, so it is a test of the actual guarantee rather than of a mock.

Payment as a state machine

Stripe hosts the card form, so the app never sees card data. That means the browser can’t be trusted to report success, and the flow is built around that:

  1. POST /api/bookings reserves seats atomically, writes a pending booking with an expiresAt hold, and returns a Checkout URL.
  2. checkout.session.completed arrives at the webhook — signature-verified, and idempotent, so a replayed delivery confirms the booking once.
  3. checkout.session.expired releases the held seats back to inventory.
  4. A background sweeper independently reclaims any pending booking past its hold, covering the case where the webhook never arrives at all.
  5. Cancelling refunds through Stripe, restores the seats, and invalidates the cached launch.

Steps 3 and 4 overlap deliberately. The webhook is the fast path; the sweeper is the one that makes the invariant hold when the fast path fails.

The rest of the backend

Money is stored end to end as integer USD cents — floats never touch a price. zod validates request bodies and the environment at boot, so a missing secret fails at startup rather than at the first request that needs it. Redis carries sessions (connect-redis), a cache-aside layer over the read-heavy catalog endpoints, and rate-limiter-flexible token buckets. Admin routes are gated on a role field checked server-side — the hidden UI is a convenience, not the control.