Skip to content
Zain Mahmood
Selected work

MediaListory

It started as MyGameList, a game tracker. It is now one library for movies, shows, anime and games, stitched from three catalogues that disagree about IDs, filters and what counts as a title.

Type
Production web app
State
Live
Role
Solo, end to end
  • Node.js
  • Express
  • Neon Postgres
  • TMDB
  • Kitsu
  • IGDB
  • Playwright

At a glance

  • Movies, shows, anime and games in one schema
  • Imports from Letterboxd, MAL and Trakt
  • Unit, smoke and Playwright suites plus Semgrep on every push

What I owned

Solo project. The Express API and provider proxies, the Postgres schema and migrations, OAuth and sessions, the no-build vanilla JS frontend, admin and moderation tools, and CI.

Decisions, and what they cost

  1. One catalog table, discriminated by media type

    Every title lives in one catalog table with a media_type, a provider and a provider_id, and a unique index on (provider, media_type, provider_id) guarantees IDs from different providers can never collide. Tracking, episode progress and custom lists reference that one table, so they work for every media type unchanged, and a new category slots in without schema churn.

    What I gave upEvery query has to carry media_type, and the row shape is the union of what four kinds of media need. Separate tables would be tidier per type and much worse for a list that mixes a film with a game.

  2. Filters come from the provider, not the page

    Each category offers only the filters its provider genuinely supports, and nothing is filtered client-side after the fact. That is why an exact title match comes back first instead of whatever happened to be on the current page of results.

    What I gave upThe four category pages are not symmetrical. Movies filter by runtime and language, anime by season and age rating, games by platform and mode, and the UI has to explain that difference instead of hiding it.

  3. The API runs the OAuth exchange itself

    The static frontend is on Vercel and the API on Render. Google and GitHub sign-in are direct OAuth2 code exchanges on the API, which then mints its own session JWT in an httpOnly cookie, so sessions survive the split deploy. Render can also serve the frontend as a same-origin fallback.

    What I gave upMore auth code to own and test than a hosted drop-in, and OAuth redirect URIs must be registered against the API origin, which is easy to get wrong on a fresh deploy.

Known limitations

  • The detail-page extras resolve provider IDs live on every view unless an optional migration is applied, which is slower and spends more upstream budget.
  • The API sleeps when idle on free hosting; a scheduled workflow pings /ready three times a week to keep it warm and surface outages.
  • ALLOW_DEGRADED lets the server boot with a broken dependency and is kept out of production by documentation, not by code.

Where it stands

In production. Episode progress, custom lists, a merged release calendar, a year-in-review, imports from Letterboxd, MAL and Trakt with a per-row match preview, taste matching between users, follows and a feed, plus admin and moderator dashboards. Every push runs unit, smoke and Playwright suites and a Semgrep scan that fails on any finding.