- Name: Dualist
- Stack: SvelteKit 2, Svelte 5 (runes), TypeScript
- Purpose: MVP that ingests a TMDb public list URL, stores a snapshot of movies in Supabase, lets the user rank via pairwise comparisons, and shows a finished permalink (read-only results).
- Create a
.envat project root with: SUPABASE_URL=… SUPABASE_SERVICE_ROLE=… TMDB_V4_TOKEN=… TMDB_IMG_BASE=https://image.tmdb.org/t/p - Install deps:
npm install - Run dev:
npm run dev
Each list item stores a frozen snapshot (title/year/poster_path) in list_items.snapshot. Why: Keeps permalinks stable over time and avoids live TMDb lookups during view. If a movie's title changes on TMDb, your ranked list still shows what you ranked.
Uses comparisons and rankings tables instead of JSONB. Why: Better for analytics, querying, and performance. Enables future features like "which movies win most often" across lists.
All comparisons stored client-side (with localStorage persistence) and only saved to database when user completes all pairs. Why: Reduces API calls, enables offline support, and provides better UX (no loading states between each choice).
Comparisons are shuffled using Fisher-Yates algorithm when initialized. Why: Randomizes the order users see pairs, preventing bias from always seeing movies in the same sequence.
Lists track source_platform, source_url, source_list_id. Why: Designed for future Letterboxd support. The ingest flow is abstracted to make adding new platforms straightforward.
- No Tailwind or inline styles. Use Svelte component-scoped
<style>blocks only. - Why: Keeps styles co-located with components and avoids external dependencies.
- Keep secrets server-only: Never expose
SUPABASE_SERVICE_ROLEorTMDB_V4_TOKENto the browser. - All Supabase writes/reads performed in server code (actions, loads, +server routes) with the service role client.
- TMDb fetching is server-only (in
lib/server/tmdb.tsor via a server function). - Why: Prevents credential leakage and ensures all data access is controlled server-side.
- Use
throw redirect(...)andthrow error(status, msg); do NOT catch and swallow these. - Don't wrap redirects inside try/catch unless you rethrow framework exceptions.
- Client navigation in comparison flow uses
window.locationfor reliability after bulk saving (redirects to results page). - Why: SvelteKit's
throwpattern is the framework's way of handling redirects/errors. Catching them breaks the framework's flow.
- TypeScript everywhere; keep types close to usage (define
Progress, load return types). - Why: Type safety catches errors early and makes refactoring safer.
- Use interactive elements for clicks in the UI (e.g.,
<button>for comparison cards). - Why: Required for keyboard navigation and screen readers.
- RLS: OFF for MVP (all DB access is server-side with service role). Can be enabled later.
lists.progress: Legacy JSONB field (will be replaced by normalized tables). Don't use for new features.filmstable: Not used in MVP. Can be added later to dedupe and enable analytics; easy to backfill from existing snapshots.
- Use TMDb v4 list API with Bearer Read Access Token (
TMDB_V4_TOKEN) to fetch public lists. - Only ingest
moviemedia_type for MVP. - Store only
poster_path(build URLs in UI withhttps://image.tmdb.org/t/p/w342{poster_path}).
- Letterboxd integration: Add support for importing Letterboxd lists (architecture already supports it via
source_platform; need to implementingest.letterboxd()similar toingest.tmdb()) - Better error states: Add
+error.sveltefor 404s and other errors (currently uses default SvelteKit error page) - Optional polish:
- Debounce/double-click guard and clearer disabled state during submit
- Remove server/client debug logs before production
- Add loading states during bulk save operation
- Add
filmstable and backfill fromlist_items.snapshotto enable cross-list analytics - Add
rankstable to persist final order per list for easier querying - Optional move of ingest into a Netlify Function or Supabase Edge Function; current code is abstracted behind
ingestTmdbListto ease that refactor