Skip to main content
JCR.DEV / DESIGN
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.

TrailVenture homepage with a destination, date and price search bar over a Palawan photograph.

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

6 sheets
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.