Untitled taste portrait
In development and unnamedA taste portrait built from your own ratings and your own words, not a watch log and not a collaborative-filtering black box. Movies, TV, games, and books in one place.
- 587+
- tests
- 4
- media types
- 5
- taste axes
- 280
- char verdict cap
The problem
Logger apps record what you watched and hand you a star rating with no room for why. Collaborative filtering recommends by proxy: people who liked what you liked also liked this. It works, but it explains nothing, and it throws away the specific thing you actually think about a title. I wanted a system that keeps my own language, the exact word I would use for a movie rather than the genre it is filed under, and treats that word as the real signal driving everything downstream.
Architecture
Data model
Everything lives in Postgres on Neon, reached through a single API-first boundary: no database write happens anywhere else in the app. A rating is stored as a 0-20 smallint, halved for display to a ten-point half-star scale. Each entry can carry a free-text flavor tag, a texture word like "devastated me" instead of a genre, lowercased and slugified for dedupe but never checked against a fixed list, plus a verdict capped at exactly 280 characters. The fingerprint weights each tag by log(1 + count) times a rating-scaled factor, so a tag used four times at a 7 can outrank a tag used once at a 10. It renders on the home page as an SVG word cloud, weight mapped straight to font size and opacity. No axis, no legend, no chart library.
Recommendations, two stages
Stage one is pure scoring, no model: top genres and flavor tags map to TMDB and IGDB keyword IDs through a hand-curated table with a fuzzy fallback, then it queries TMDB, IGDB, and Open Library in parallel and ranks the results by genre and keyword overlap. Stage two hands that candidate list to an LLM along with my own verdicts, and asks it to pick from the candidates and ground its reasoning in my specific words. It cannot invent a title outside the pool; a hallucination guard drops any pick that does not match a real stage-one candidate. That curation runs on Gemini Flash, moved off Claude to cut cost. Claude's job is narrower and deliberate: the deep portrait, the one extended-thinking surface, reading the whole collection and writing pattern-level insight cards instead of picking titles.
Beyond recommendations
The same collection feeds a much wider surface than recommendations, all of it reading off the same entries and tags, one data model underneath:
- A three-lens portrait page: five bipolar taste axes anchored to published media-psychology instruments, an adaptive stats lens computed from your own distribution rather than a global cutoff, and a prose lens
- A four-tab atlas of the tag landscape, from terrain contours to a pan-and-zoom constellation
- Mixtapes, ordered two-sided playlists of your own titles that freeze forever when you burn them
- Eras, immutable taste snapshots auto-generated at the solstices by a cron job
- A year letter, a long essay grounded only in your own vocabulary and your fingerprint's drift, structurally barred from citing titles so it has no hallucination surface
- Two-user mirror pairs, public verdict pages with structured data, and Goodreads/StoryGraph import
Privacy, the opposite of CivicRadar
It is deliberately the structural opposite of CivicRadar on privacy. CivicRadar stays localStorage-only because its users' risk is ending up on a list; this one is server-side with accounts because taste is meant to be shared: public verdict pages, comparisons, mixtape links. The privacy architecture follows from who is at risk and what the product is for, not a single dogma applied everywhere.
The deliberate choice
The choice I would defend is using your own words instead of other people's behavior. Collaborative filtering needs a lot of users behaving similarly to say anything useful, and even then it can only tell you what correlates, not what matters to you about a title. This system asks for one texture word and one 280-character verdict per rating instead, and both become load-bearing: the tag drives the fingerprint's weighting, the verdict gets fed back to the curation model as grounding text it has to quote from. It is a smaller signal than a million other users' watch histories, but it is mine, kept legible instead of averaged away.
Stack
- Next.js 16 (App Router), React 19, TypeScript (strict)
- Tailwind, custom cream/ink/ember palette, Fraunces as the editorial display face
- PostgreSQL on Neon via Drizzle ORM, API-first (all persistence through /api/v1)
- TanStack Query for client state
- Auth.js: Google OAuth and magic-link email
- Gemini Flash (recommendation curation, essays), Claude Sonnet (deep portrait, the one extended-thinking surface)
- TMDB, IGDB, Open Library
- Vercel (target host), Sentry, Vercel Analytics
Status & links
Fully built and tested locally, never deployed. The deployment runbook is written; what remains is one credentialed session and a smoke test.