Building Media Tracker for TV and movie watchlists
The Motivation Behind the Project
Media Tracker is built for people who want a cleaner way to keep up with TV shows and movies. It brings watchlists, episode updates, and backup into one mobile experience instead of scattering them across different tools.
Core Features and Design Goals
- TMDB-backed discovery: a familiar catalog of movies and series.
- Customized watchlists: a place to keep track of what is in progress or next up.
- Push notifications: alerts for new episodes so the app stays useful after setup.
- Data backup: a practical detail that matters once the list gets large.
Technical Implementation
- Built a real-time cloud sync engine using Supabase Realtime Postgres Changes: a local MMKV storage listener debounces writes and pushes Zustand state to Supabase, while a Realtime subscription channel pulls remote changes from other devices, with an infinite-loop guard flag to prevent echo writes.
- Implemented a cold-boot conflict resolver that on app launch compares the local MMKV edit timestamp against the Supabase updated_at — if local is newer (offline edits), it pushes up; if cloud is newer (another device pushed), it pulls down and triggers Zustand persist.rehydrate() to live-update all React components.
- Designed a personalized recommendation engine that analyzes the user's genre distribution across tracked shows and movies, then fetches TMDB's similar/recommendation endpoints for completed and currently-watching items, deduplicates against the existing watchlist, and sorts by vote average.
- Built a background backup system with expo-background-fetch that auto-uploads watchlist state to Google Drive on configurable intervals (6h to weekly), with network-type preferences (WiFi-only/any), first-backup merge protection to avoid overwriting existing Drive data on reinstalls, and automatic Android widget date-label refresh in the same background task.
Challenges I Faced Along the Way
The hardest part of Media Tracker was building the real-time sync without creating infinite loops. When device A writes a change to Supabase, the Realtime subscription on device B picks it up and writes it to local state. But that local state change triggers a listener that tries to push back to Supabase — creating an echo. I solved this with a guard flag that temporarily disables the push listener when a pull is in progress. The cold-boot conflict resolver was equally tricky: when the app launches after being offline for days, it needs to compare local and remote timestamps and decide which version wins, without losing any data from either side. Edge cases like simultaneous edits on different devices required a last-write-wins strategy with per-field timestamps rather than per-record timestamps.
The Technology Stack
Built with Expo SDK 54, Expo Router, TypeScript, Supabase, and Zustand with MMKV persistence. Uses React Query with PersistClient for offline caching, Nativewind for Tailwind-based styling, react-native-android-widget for home screen widgets, and a full bilingual i18n system. Cloud sync runs through both Supabase Realtime (live) and Google Drive (backup), with expo-background-fetch for scheduled tasks.
What I Would Do Differently Next Time
The recommendation engine was a feature I built late in development, and it shows. The current implementation makes too many API calls to TMDB for similar/recommended content, which is slow and hits rate limits. A better approach would be to compute genre affinity scores from the local watchlist and use a single discover endpoint with genre filters instead of querying per-show recommendations. I would also build the sync engine as a standalone module with its own test suite rather than embedding it in the app's state management layer.
Final Reflections
Media Tracker is a good example of building around an existing habit and refining it into a smoother daily workflow. The sync engine alone taught me more about distributed state management than any tutorial could — the edge cases in real-time sync between multiple devices are genuinely complex, and the only way to find them is to use the app daily across multiple phones.