ProjectWeb Development · 2024
Movie Ticket Booking Webapp
A hackathon movie booking app on Next.js and PostgreSQL, with a TMDB catalog, trailers, showtimes and a seat picker. Payments were out of scope.
- Next.js
- PostgreSQL
- Drizzle
- Recoil
- AWS
- Zod
- Auth.js
- ShadCN
- AWS S3
- Docker
- Resend

Web Development · Project
QuickNix is a movie booking web app built for the StarHack hackathon. It lists what is showing using the TMDB API and lets you search it. A movie page carries the poster, the overview, the vote average and a theater and showtime picker. It is a hackathon build, and it stopped where the clock did.
The README says as much in its own words. Payments and the reservation system were never finished. Everything up to checkout works, and I would rather leave that written down than imply a flow that is not there.
# catalog rows come from TMDB, not a fixtureGET /api/v1/movies?query=inter&limit=10 3 resultsGET /movies/9f2c1a vote average 8.4, 2 genresGET /buytickets showtime 7:30 PMseat picker: executive-D8, executive-D9selection is component state, total 560checkout: no route writes a reservation rowHow the catalog fills
An administrator pastes an IMDb id into the add-movie form, and actions/tmdb.js calls the TMDB find endpoint with external_source=imdb_id. The first match is flattened into the shape of the movies table, with poster and backdrop paths rewritten onto the w500 image host. GET /api/v1/movies then reads that table through Drizzle, pages the result, sorts by releaseDate and matches titles with ilike. It also excludes rows whose poster path resolved to the literal /None, which is what TMDB hands back for a title with no artwork. The same route mirrors documents into a Typesense movies collection faceted on genre, language and vote average.
The seat picker
Choosing a seat is the part of a ticket flow anyone actually remembers, and it is the least real part here. components/app/buyTickets/SeatSelection.jsx renders from a seatData constant compiled into the component. Premium rows L, K and J cost 300 rupees, executive rows I down to C cost 280, and rows B and A cost 260. Selection is local useState keyed by a section-rowNumber string, and the total is a reduce over the selected ids. The theaters table does carry a seats JSONB column, defaulted to an empty array. Nothing reads it. The map on screen belongs to no theater in the database.
Five tables
lib/db/schema/ holds movies, theaters, showtimes, reservation and user, all Drizzle over Postgres on Neon. A showtime joins a movie to a theater and carries ticketPrice, startTime and endTime. A reservation holds its showtime, its user, seats as JSONB, a unique orderId, a total, customer name and phone, and a status of pending, confirmed or cancelled. Theaters store both address and seats as JSONB. Keeping seats in JSONB on two tables is the decision I would revisit first. It made a seat map easy to imagine under hackathon pressure, and it makes an honest availability query awkward.
Choices under a clock
- Server actions call the application's own REST routes over HTTP through
NEXT_PUBLIC_APP_URLinstead of reading the database. One access shape then serves the pages and the Bruno collection inbruno/alike. Every page load also pays a network hop back to itself. - Typesense sits beside Postgres rather than replacing the
ilikequery. Facets on genre and language become cheap. Two stores now have to stay in step, and only the write path in/api/v1/moviesdoes that. - Authentication is Auth.js, and
middleware.jsis one re-export of it. Sign-in, sign-up and a gated admin panel came together quickly. The middleware checks no roles, so anything role-specific has to happen in the page itself. - Recoil is in the stack for client state, and
lib/atoms/userDetailsAtom.jsis what it holds. The seat picker never reached it and stayed on component state.
Running it locally
Setup is short, and pnpm dev binds port 3001 rather than 3000.
pnpm install
pnpm dev
curl "http://localhost:3001/api/v1/movies?query=inter&limit=10"
That last request is the quickest check that both halves of the catalog path worked. The environment needs DRIZZLE_DATABASE_URL for Neon, TMDB_API_KEY, AUTH_SECRET, the four TYPESENSE_ variables and NEXT_PUBLIC_APP_URL, since the server actions use that one to reach their own routes. Schema commands are pnpm db:generate, pnpm db:migrate and pnpm db:push.
Where it stopped
The payment page renders a checkout screen with a hardcoded film, a hardcoded price and three payment method radios. No gateway is wired behind it, and no code writes a row into reservation. Both gaps are listed under known issues in the README. pnpm db:seed is stale as well, since it inserts movie columns such as director and trailerUrl that the schema no longer has. The README advertises trailers, and no page ever rendered one. What does work is the catalog, search, movie detail, cinema list and admin panel. It runs in Docker behind a Caddy container proxying to app:3001, hosted on EC2, and is MIT licensed.
Next projectSearch Infrastructure