MOVIENIGHT
A new way to watch together.
The one stop shop for everything you need for a successful Movie Night. Personal AI recommendations, wishlists, badges, and the pick engine. The most fun way to decide what to watch together.
Director’s Note
Once Upon a Friday Night
You know the scene. Snacks are out, the couch is claimed, the TV’s on — and somehow everyone in the room has actually agreed to watch a movie together. Small miracle. Then you open your streaming app of choice and get hit with an endless scroll of weirdly specific, occasionally unhinged categories. The one movie someone actually wants is on a service nobody’s logged into. The compromise pick costs four bucks to rent. The safe pick got watched two weeks ago. And every suggestion thrown out is one wrong word away from quietly turning the night into a referendum on everyone’s taste.
“The fact that some choice is good doesn’t necessarily mean that more choice is better.”
Enter Movie Night. What started as a personal passion project to solve the "what should we watch problem" that has evolved into an all-encompassing movie hub. Details, trailers, actor pages, watchlist, fun customizable badges, reviews, AI recommendations, and every other thing you could hope for in a movie app.
But the real heart of it is the pick engine. Everyone in the room nominates, everyone marks ready, and the result lands at the same instant over a single live connection. Endless ways to decide — a random draw, a bracket battle, a ranked vote, or a two-player veto duel — so the room can match the mode to the night. The whole idea is to take the part of the evening that used to suck and start arguments and turn it into the most fun minute on the couch. Press play. Eat the snacks before they get cold.
Goals
What problem does this solve?
End the "what should we watch" stall. Everyone nominates, the room decides, and nobody has to argue.
Pull every movie from a single source of truth. Where to watch, how to watch, without ever leaving the site.
Make the picking fun, realtime updates and cursor ghosts make deciding a game, not a chore.
Personalized AI recommendation that actually matters. Real sentiment analysis. Vector storage and tokenization.
Live rooms
Anatomy of a pick event
One WebSocket per event. Every member sees the same state at the same instant.
- 01
Create the event
The host picks an engine, sets the nominations-per-user cap, and names the room. The event row goes into Postgres and a join link gets shared.
- 02
Members join
Visiting the link auto-joins the user (idempotent). Each member gets a color and shows up in the lobby live.
- 03
Nominate
Members search and nominate movies. The client sends only an identifier — the server re-resolves every display field so the room cannot be poisoned.
- 04
Mark ready
Everyone flips their ready state. The room watches every state change tick in over a single WebSocket per event.
- 05
Run the engine
Once the room is ready the chosen engine takes over: a random draw, a bracket battle, a ranked vote, or a rolling-random two-player run.
- 06
Reveal
The result lands as a broadcast. Everyone sees the winning movie at the same instant, with poster, runtime, and a "play locally" link when the file is available.
Pick your poison
Challengers
Each engine lives behind a shared interface so the only limitation is our own creativity.
Random Pick
One draw, no drama.
Everyone nominates a set number of movies, the engine picks one at random. Fast, fair, no negotiation.
Battle
Single-elimination bracket.
Movies get paired off and the room votes round by round. Random seeding keeps the bracket interesting. Winner is the last one standing.
Ranked
Weighted positional vote.
Everyone orders the whole pool from most to least wanted. Top picks count more, and the highest weighted total wins.
Rolling Random
Two-player veto duel.
A random pick is drawn each round and removed from the pool. The picker loses a slot, the other gains one. The event stays open until someone locks in a final winner.
Casting agent
(AI) Companion
A ReAct agent that chains catalog, local library, and viewing-history tool calls to pick one movie with a reason.
Example prompt
“A visually stunning movie I probably haven’t seen, around two hours, that I can watch with my partner.”
Type how you feel
"Something like Arrival but more playful." "A ninety minute comedy for a tired Tuesday." The prompt is freeform.
ReAct agent kicks off
The recommender service spins up an agent that can call catalog search, library lookups, and viewing-history lookups as tools.
Chain of tool calls
The agent reasons, calls a tool, reads the result, reasons again. A run can legitimately take several minutes.
One movie, one reason
The agent returns a single pick with a written reason. The web app resolves it through the catalog and renders the full card.
Catalog
One source of truth
Every poster, runtime, and credit comes from the same place. The client never sends descriptive fields — it sends an id.
Single id in
Everything is keyed on one canonical identifier. Lists, picks, library cross-references, deep dives — all of it.
Resolve to canonical
A single resolver builds the canonical shape: title, year, poster, overview, genres, cast, crew, runtime, audience rating.
Stale-while-revalidate cache
Cached for a day with a seven-day stale window. The cache hands back the last-known-good response immediately and refreshes in the background.
Local media overlay
If the movie is in the local library, the resolver tags the record with a local id so the post-decision UI can offer a play link.
Long form
Deep dives
The recommender can also write long-form essays on a single film: history, themes, key people, sources. Each essay has a research hint (low / medium / high) that controls how much background the agent gathers.
- Sourced quotes and citations attached to each section
- Key people surface director / writer / cinematographer cards inline
- Refresh priority lets the agent re-research when stale
Personal taste
Diary sync
Each user can import their viewing history from a diary export or sync against a public username. The diary feeds the recommender so suggestions account for what you’ve already watched and how you rated it.
- Zip import or background sync per user
- Diary entries are matched against the catalog on ingest
- Rewatches, ratings, and tags all flow into the recommender
Production stills
Now Showing
Scroll to part the curtains.

Reel · Landing
Neon marquee, starfield, admit-one ticket

Reel · Browse
Catalog grid — posters, watch status, filters

Reel · Movie details
Cast, crew, runtime, audience rating

Reel · More details
Reviews, reactions, related films

Reel · Pick event
Nominations streaming in, ready flags flipping

Reel · AI shortlist
Multiple AI picks at a glance

Reel · AI companion
Freeform prompt — one movie, one reason

Reel · Deep dives
The long-form essay library

Reel · Deep dive
Themes, history, key people

Reel · Deep dive
Sources, citations, refresh priority
Reel 01 / 00
Everything Everywhere All at Once
Real-time pick events that everyone sees at the same instant
A handful of decider engines for different kinds of nights
Full movie catalog: posters, cast, crew, collections, runtime
AI recommender that takes a freeform prompt and picks one movie
Personal viewing-history import to keep recommendations on taste
Long-form "deep dive" essays generated per movie
Movie lists, watchlists, and a "wanted" request queue
Per-user stats, badges, and a personal diary
Cross-references your local media library with the catalog
Docker — deploy anywhere from a Raspberry Pi to a cloud VM
Smart caching keeps the catalog fast and current
Admin-gated room management and metadata editing
Challenges & solutions
Challenge 01
Real-time rooms over a single WebSocket
Multiple users in a pick event need to see each other join, watch nominations land, see ready states flip, and watch a winning movie reveal at the same instant. Polling would be wasteful and would still feel laggy. Long-lived connections needed careful lifecycle handling for joins, leaves, reconnects, and the moment a result is decided.
Solution
One WebSocket connection per pick event. Server validates every C2S message, applies the state change against Postgres, and broadcasts the updated snapshot to everyone in the room. Engine-specific messages (battle rounds, ranked submissions, rolling random finalize) live alongside core room messages without leaking through the type system.
Challenge 02
Endless decider engines, one room
The room had to support more than one way to decide. A single random draw is fast but flat. A bracket battle is more fun but takes longer. Ranked voting reads everyone's preference order. Two-player rolling random changes the pool every round. Each mode has its own rules, its own messages, and its own UI state.
Solution
Each engine is its own module behind a shared interface. Engine-specific WebSocket messages live in separate type files so the core room never has to know what "rolling_random:finalize" means. Engine config is stored as opaque JSON on the event row and only read by the engine that owns it.
Challenge 03
Long-running AI agent over HTTP
The recommender service runs a ReAct agent that chains many tool calls — catalog searches, local library lookups, viewing-history scans. Single runs can legitimately take several minutes. Node's default fetch caps both headers and body at five minutes, which surfaces as 502s or 504s right when the agent is about to land an answer.
Solution
A dedicated undici Agent with a ten-minute headers and body timeout, mirrored on the Node HTTP server. The Recommender call is wrapped in a helper that surfaces useful cause information (ECONNREFUSED, DNS, AggregateError walking) instead of the generic "fetch failed" error you get from undici by default.
Challenge 04
Catalog hydration cost vs. freshness
Hitting the upstream catalog on every page render would burn quota fast and add latency for the user. But cached entries go stale: posters get updated, ratings drift, a movie gets a new tagline. The catalog needed to feel canonical without paying the upstream cost on every request.
Solution
Stale-while-revalidate caching keyed on a canonical id, with a one-day TTL and a seven-day stale window. The cache serves the last-known-good response immediately and revalidates in the background. Hot endpoints get wrapped in a cache helper; long-tail endpoints fall back to direct fetches with a shorter TTL.
Roll credits
End credits
The cast and crew that made Movie Night possible. Yes, even the divs.
A MOVIE NIGHT
PRODUCTION
Directed by
SvelteKit
Produced by
Postgres · Drizzle ORM
Original concept by
Friday, 8:45pm, nobody can pick a movie
Crew
Cast · in order of appearance
Special thanks
- The 17 hours spent centering a single <div>
- The 43 "just one more commit" commits
- Stack Overflow, circa 2014
- That one Postgres index that fixed everything
- Caffeine. And more caffeine.
- The friend who said "what if it was a bracket"
- pnpm, for being on time
- localhost:3000, our second home
In loving memory of
- The Redux store we replaced
- Server-side rendering, briefly
- Six different attempts at a recommender prompt
- Every npm package that got deprecated mid-build
- Prop drilling (1995 – 2024)
No pixels were harmed in the making of this application. All flexbox stunts were performed by trained professionals on a closed track. Any resemblance to working WebSocket code is purely intentional.
FIN
© 2026 josh doeswork pictures
Box Office
Request a Screener
Reserve a seat for a private screening of Movie Night. Fill out the request below and the projectionist will be in touch.
Screening request
Movie Night