02.1Major Project / Solo Developer6 sheets
TrailVenture: Tour Package Booking Platform
A booking platform for tour packages across the Philippines and beyond. Travellers search by destination, date and budget, compare package tiers and day-by-day itineraries, book a start date for a group, and pay through Stripe. Most of the work is in the guarantees around the money rather than the browsing on top of them.

Destination, date and budget in a single entry point
Sheet 1 of 6
Sheet 1 of 6. Destination, date and budget in a single entry point
- Tech stack
- Django
- Django REST Framework
- PostgreSQL
- Redis
- Celery
- Stripe
- Next.js
- React
- TypeScript
- Tailwind CSS
- shadcn/ui
- TanStack Query
- Docker
- Playwright
- pytest
- GitHub Actions
- Skills
- API security
- Payment integration
- System design
- Testing strategy
- Accessibility
- CI/CD
- Context
- Booking is the part of a travel site where mistakes cost real money. A price shown to a traveller has to be the price they are charged, a payment that half-succeeds has to resolve one way or the other, and a seat held during checkout can be neither sold twice nor held forever. The browsing experience is the easy half.
- Role
- Solo — architecture, backend, frontend, tests and deployment.
- Approach
- A Django API behind a Next.js frontend, with the browser only ever talking to one origin: the web app forwards Django’s paths at request time, so session and CSRF cookies stay first-party and no token is ever kept in localStorage. Views stay thin — writes go through a service layer, reads through selectors tested for query counts. Roles are decided entirely by the server, because a client setting its own is the most common way a booking system gets compromised.
- Money
- Prices are calculated on the server and stored as integer centavos, so nothing is lost to floating point and the client never has a say in what it is charged. A booking holds its price for thirty minutes and checkout refuses an expired hold. Stripe Checkout sessions carry an idempotency key, so a repeated request returns the existing session instead of charging twice. The webhook verifies Stripe’s signature, records every event id before acting on it, and ignores replays — and unexpected errors return 5xx on purpose, so Stripe retries rather than a failed payment vanishing quietly. Nothing deletes a booking; unpaid holds expire on a schedule and stay on record.
- Caching
- Catalog responses are cached in Redis under keys carrying a version number, and any write to a catalog model bumps that version, so every dependent response becomes unreachable at once. The alternative — deleting the specific keys a write affects — means being right about that mapping every single time, which is exactly where stale-cache bugs live. A test asserts a cache hit costs zero database queries and a miss no more than four.
- Correctness
- 108 API tests, 50 unit tests and 132 browser tests across desktop, mobile and dark mode, with CI running the browser tests against a live API, Postgres, Redis and a mail server rather than mocks. The tests pin the rules that matter: a signup cannot set its own role, a webhook replay changes nothing, an expired hold cannot be paid for, and a review requires a booking that was paid for and has already started. axe checks every public page in CI, and Lighthouse reports 100 for accessibility and SEO.
- The Trade-off
- It runs entirely on free tiers — two containers and PostgreSQL on Northflank, Redis on Upstash, images on Cloudinary, mail through Mailjet, behind a custom domain with automatic TLS. That tier has no always-on worker, so background tasks run inside the request and hold expiry runs as a scheduled command instead of a queue. Dropping the worker was only safe because a hold is derived from a timestamp and checkout re-checks it: the scheduled job is housekeeping, not the mechanism. Working out which dependencies were load-bearing and which were convenience is what made the free deployment possible.
- What is not built yet
- Host and administrator roles, seat capacity per departure, refunds, two-factor authentication, a wishlist and an itinerary map are designed and accounted for in the data model, but not implemented. It is up as an early access demo: trips are demo data and payments run in Stripe test mode, stated plainly on the site itself.
