My Watch Tracker is a small, deliberately focused watchlist for movies and TV shows. It keeps watched and unwatched titles in one place, adds poster art from TMDB, and offers explainable recommendations when the watch history is still small.
The public watchlist is browseable by anyone. Only the authenticated admin session can add, edit, or delete entries. The app runs on Flask and PostgreSQL as a Python function on Vercel.
Live deployment: https://movie-tracker-umber-sigma.vercel.app
The first version was a straightforward CRUD app. The interesting work came after that: making failed database connections safe, keeping the TMDB API from being hit without limits, protecting every write route, and making the recommendations explainable instead of pretending I had enough data for a serious machine-learning system.
I found the production database connection especially easy to get wrong. The
Vercel deployment needs the Supabase Shared Pooler transaction-mode URL on port
6543, with the project-qualified connection identity from Supabase's Connect
settings. The Flask database client uses a five-second connection timeout and
turns database failures into safe responses instead of exposing driver errors.
The other awkward part was the recommendation cold start. My watchlist is too small for a meaningful production-quality metric, so I store TMDB IDs, media types, and genre IDs and use a deterministic content-based baseline. It is useful and inspectable, but I do not describe it as an evaluated ML system.
If I started again, I would settle the migration and production connection workflow earlier, before spending as much time on UI polish. If this grew beyond a personal app, I would also move authentication and row permissions to a real multi-user identity system, and use shared infrastructure for TMDB caching and rate limiting instead of relying on one warm serverless instance.
- I can rate entries from 1 to 10 with a structured score panel and strength bar.
- Exact 10/10 entries get a gold card outline, gold score panel, and
Top tierlabel. - I can mark an entry as Watched or Want to Watch.
- Movies and TV shows appear in separate sections with numbered editorial headings.
- I can edit ratings and status, or delete entries from the authenticated admin view.
- Anyone can browse the public watchlist; the admin area is password-protected.
- The UI includes progressive loading skeletons, responsive layout rules, and reduced-motion support.
- The recommendations page uses stored TMDB genre metadata to produce deterministic, explainable suggestions.
The repository includes health, authentication, mutation, database, loading,
recommendation, and usage-measurement tests. Run them locally with
python -m pytest before sharing a build.
Production verification should be recorded separately from this README. Do not
publish deployment IDs, commit identifiers, admin credentials, private pooler
details, or raw usage counts here. The admin password belongs in
ADMIN_PASSWORD_HASH; the internal verification record lives in
docs/verification.md.
- This is a single-admin Flask session, not a multi-user account system. I have not integrated Supabase Auth or RLS.
- The recommender is a deterministic content-based baseline. It uses TMDB and stored genre metadata, falls back to popular picks when the history is sparse, and does not yet have a real production precision metric.
- TMDB caching and rate limiting are limited to each warm serverless instance. Global enforcement would need shared state.
- Usage counters describe aggregate activity events rather than unique people or registered users.
- Browser keyboard, screen-reader, and responsive visual review still need a proper browser pass.
I do not use an external analytics provider. The app stores only daily counts for successful public views, adds, and recommendation views. It does not store IP addresses, user agents, referrers, account IDs, or browser identifiers.
Keep exact production counts in private verification notes rather than in a public README.
On 2026-08-08, I ran a reproducible offline benchmark using 585 real TMDB records: a synthetic 200-entry watchlist, a chronological 160/40 train/holdout split, and a 400-item candidate pool containing 40 held-out positives and 360 unseen benchmark negatives.
| Metric | Result |
|---|---|
precision@1 |
0.000 |
precision@5 |
0.000 |
precision@10 |
0.000 |
This is a synthetic offline baseline, not a production or user-satisfaction metric. The benchmark negatives are unseen titles rather than verified dislikes, and TMDB popularity pages overrepresent visible and recent titles. The result is still useful: this simple feature-union recommender did not recover any held-out titles in the top 10 for this split.
| Layer | Tool |
|---|---|
| Language | Python 3.10+ |
| Framework | Flask |
| Database | PostgreSQL (Supabase) |
| Cover art | TMDB API (free) |
| Hosting | Vercel (Python function) |
| Runtime | Vercel Python runtime |
git clone https://github.com/YOUR_USERNAME/movie-tracker.git
cd movie-tracker
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txtRun the automated checks with:
python -m pytestI pin the direct dependencies in requirements.txt; the update process is in
docs/dependency-update.md.
Copy .env.example to .env and replace the placeholders with local secrets
or service credentials. I keep .env untracked; production values belong in
Vercel's encrypted environment variables.
ADMIN_PASSWORD_HASH must contain a Werkzeug password hash, not the plaintext
admin password. Generate one interactively with:
python -c "from getpass import getpass; from werkzeug.security import generate_password_hash; print(generate_password_hash(getpass('Admin password: ')))"To rotate credentials, generate a new hash, replace ADMIN_PASSWORD_HASH in
the local or Vercel environment, and redeploy. I do not retain or document the
old plaintext password.
The Flask session cookie is HTTP-only and SameSite=Lax. Vercel and other
production environments also set the cookie's Secure flag. Rotating
SECRET_KEY invalidates signed sessions and makes administrators log in again.
For Vercel, DATABASE_URL should use the Supabase Shared Pooler
transaction-mode connection on port 6543. The five-second connection timeout
is deliberate because a serverless request should fail promptly when the
database is unavailable.
TMDB searches use a five-minute in-process cache and limit uncached searches to 30 requests per minute per warm application instance. That protects the quota on one instance; a shared cache and rate-limit store would be needed for a scaled deployment.
This project uses the TMDB API but is not
endorsed or certified by TMDB. TMDB supplies title metadata and poster images;
I never expose TMDB_API_KEY to the browser.
Search and discovery responses are cached for five minutes per warm instance. When I add an entry, I persist the selected TMDB ID, media type, and genre IDs for recommendations. Existing entries are not silently refreshed, so stale or missing metadata can leave the recommender in its documented cold-start state. See TMDB's API FAQ for the current attribution and API-use requirements.
The Flask server is currently the mutation boundary for this app. I have not added a tested RLS policy that matches the Flask session and direct-Postgres access path, so I do not present database-level row authorization as a feature.
For the remaining work and the evidence behind these decisions, see the
BACKLOG.md,
docs/architecture.md,
docs/local-development.md,
docs/operations.md, and
docs/usage-measurement.md.